@oxy-hq/sdk 2.1.0 → 2.3.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/email.cjs ADDED
@@ -0,0 +1,14 @@
1
+ // @oxy/sdk - TypeScript SDK for Oxy data platform
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
3
+ let preact = require("preact");
4
+ let preact_render_to_string = require("preact-render-to-string");
5
+
6
+ //#region src/email.ts
7
+ /** Render an email template component to an HTML string. */
8
+ function render(Component, props) {
9
+ return (0, preact_render_to_string.render)((0, preact.h)(Component, props));
10
+ }
11
+
12
+ //#endregion
13
+ exports.render = render;
14
+ //# sourceMappingURL=email.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.cjs","names":[],"sources":["../src/email.ts"],"sourcesContent":["// `@oxy-hq/sdk/email` — render an email template to an HTML string for use in\n// an Oxy Function's `ctx.email.send`.\n//\n// You write email templates as plain JSX components (point JSX at preact with\n// `jsxImportSource: \"preact\"` in the template's tsconfig — see the examples),\n// then:\n//\n// ```ts\n// import { render } from \"@oxy-hq/sdk/email\";\n// import { Welcome } from \"../emails/Welcome\";\n// await ctx.email.send({ to, subject, html: render(Welcome, { name }) });\n// ```\n//\n// Rendering uses preact-render-to-string — pure JS, no react-dom/server, no node\n// builtins, no Web Streams — so it bundles under esbuild `--platform=neutral`\n// and runs inside the Oxy Functions isolate (React Email / react-dom cannot).\n// Kept in a separate subpath entry so it never bloats the main SDK bundle.\n\nimport { type ComponentType, h } from \"preact\";\nimport { render as prerender } from \"preact-render-to-string\";\n\n/** Render an email template component to an HTML string. */\nexport function render<P extends Record<string, unknown>>(\n Component: ComponentType<P>,\n props: P\n): string {\n return prerender(h(Component, props));\n}\n"],"mappings":";;;;;;;AAsBA,SAAgB,OACd,WACA,OACQ;CACR,yDAAmB,WAAW,KAAK,CAAC;AACtC"}
@@ -0,0 +1,8 @@
1
+
2
+ import { ComponentType } from "preact";
3
+ //#region src/email.d.ts
4
+ /** Render an email template component to an HTML string. */
5
+ declare function render<P extends Record<string, unknown>>(Component: ComponentType<P>, props: P): string;
6
+ //#endregion
7
+ export { render };
8
+ //# sourceMappingURL=email.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.d.cts","names":[],"sources":["../src/email.ts"],"mappings":";;;;iBAsBgB,OAAO,UAAU,yBAC/B,WAAW,cAAc,IACzB,OAAO"}
@@ -0,0 +1,8 @@
1
+
2
+ import { ComponentType } from "preact";
3
+ //#region src/email.d.ts
4
+ /** Render an email template component to an HTML string. */
5
+ declare function render<P extends Record<string, unknown>>(Component: ComponentType<P>, props: P): string;
6
+ //#endregion
7
+ export { render };
8
+ //# sourceMappingURL=email.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.d.mts","names":[],"sources":["../src/email.ts"],"mappings":";;;;iBAsBgB,OAAO,UAAU,yBAC/B,WAAW,cAAc,IACzB,OAAO"}
package/dist/email.mjs ADDED
@@ -0,0 +1,13 @@
1
+ // @oxy/sdk - TypeScript SDK for Oxy data platform
2
+ import { h } from "preact";
3
+ import { render as render$1 } from "preact-render-to-string";
4
+
5
+ //#region src/email.ts
6
+ /** Render an email template component to an HTML string. */
7
+ function render(Component, props) {
8
+ return render$1(h(Component, props));
9
+ }
10
+
11
+ //#endregion
12
+ export { render };
13
+ //# sourceMappingURL=email.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.mjs","names":["prerender"],"sources":["../src/email.ts"],"sourcesContent":["// `@oxy-hq/sdk/email` — render an email template to an HTML string for use in\n// an Oxy Function's `ctx.email.send`.\n//\n// You write email templates as plain JSX components (point JSX at preact with\n// `jsxImportSource: \"preact\"` in the template's tsconfig — see the examples),\n// then:\n//\n// ```ts\n// import { render } from \"@oxy-hq/sdk/email\";\n// import { Welcome } from \"../emails/Welcome\";\n// await ctx.email.send({ to, subject, html: render(Welcome, { name }) });\n// ```\n//\n// Rendering uses preact-render-to-string — pure JS, no react-dom/server, no node\n// builtins, no Web Streams — so it bundles under esbuild `--platform=neutral`\n// and runs inside the Oxy Functions isolate (React Email / react-dom cannot).\n// Kept in a separate subpath entry so it never bloats the main SDK bundle.\n\nimport { type ComponentType, h } from \"preact\";\nimport { render as prerender } from \"preact-render-to-string\";\n\n/** Render an email template component to an HTML string. */\nexport function render<P extends Record<string, unknown>>(\n Component: ComponentType<P>,\n props: P\n): string {\n return prerender(h(Component, props));\n}\n"],"mappings":";;;;;;AAsBA,SAAgB,OACd,WACA,OACQ;CACR,OAAOA,SAAU,EAAE,WAAW,KAAK,CAAC;AACtC"}
package/dist/index.cjs CHANGED
@@ -176,6 +176,52 @@ async function getCustomerAppDebug(resolved) {
176
176
 
177
177
  //#endregion
178
178
  //#region src/customer-app/errors.ts
179
+ /**
180
+ * Error thrown by all customer-app hooks when an API call returns a
181
+ * non-2xx response. Carries the structured `code` + `hint` the server
182
+ * emits so bundle UIs can render an actionable message instead of
183
+ * "404: { ...json... }".
184
+ */
185
+ var OxyApiError = class extends Error {
186
+ constructor(opts) {
187
+ const base = opts.message || `HTTP ${opts.status}`;
188
+ const code = opts.code ? ` [${opts.code}]` : "";
189
+ const hint = opts.hint ? `\n\n${opts.hint}` : "";
190
+ super(`${base}${code}${hint}`);
191
+ this.name = "OxyApiError";
192
+ this.status = opts.status;
193
+ this.code = opts.code ?? null;
194
+ this.hint = opts.hint ?? null;
195
+ }
196
+ };
197
+ /**
198
+ * Read a non-2xx response from oxy and return an `OxyApiError`.
199
+ * Parses the JSON envelope when present; falls back to raw text
200
+ * (truncated to 240 chars so a runaway HTML error page doesn't
201
+ * dominate the bundle UI).
202
+ */
203
+ async function apiErrorFromResponse(resp) {
204
+ let body;
205
+ let raw = "";
206
+ try {
207
+ raw = await resp.text();
208
+ body = raw ? JSON.parse(raw) : void 0;
209
+ } catch {}
210
+ if (body && typeof body === "object") {
211
+ const b = body;
212
+ return new OxyApiError({
213
+ status: resp.status,
214
+ message: typeof b.message === "string" ? b.message : `HTTP ${resp.status}`,
215
+ code: typeof b.code === "string" ? b.code : null,
216
+ hint: typeof b.hint === "string" ? b.hint : null
217
+ });
218
+ }
219
+ const snippet = raw.length > 240 ? `${raw.slice(0, 237)}…` : raw;
220
+ return new OxyApiError({
221
+ status: resp.status,
222
+ message: snippet || `HTTP ${resp.status}`
223
+ });
224
+ }
179
225
  const ARCH_DOC = "internal-docs/customer-apps.md";
180
226
  /** Interpret a thrown error as a structured report for UI display. */
181
227
  function interpretCustomerAppError(err) {
@@ -422,6 +468,24 @@ function validateFunctions(raw) {
422
468
  fn.cache = { ttlSeconds: ttl };
423
469
  }
424
470
  }
471
+ if (value.retries !== void 0) {
472
+ const r = value.retries;
473
+ if (!isRecord(r)) throw new Error(`oxy-app.json: function "${fnName}" \`retries\` must be an object`);
474
+ const retries = {};
475
+ for (const key of [
476
+ "maxAttempts",
477
+ "minTimeoutMs",
478
+ "maxTimeoutMs"
479
+ ]) {
480
+ const n = r[key];
481
+ if (n !== void 0) {
482
+ if (typeof n !== "number" || !Number.isInteger(n) || n < 1) throw new Error(`oxy-app.json: function "${fnName}" \`retries.${key}\` must be a positive integer`);
483
+ retries[key] = n;
484
+ }
485
+ }
486
+ fn.retries = retries;
487
+ }
488
+ if (value.inputExample !== void 0) fn.inputExample = value.inputExample;
425
489
  const hasSchedule = fn.schedule !== void 0;
426
490
  const hasAirway = fn.airwayStep !== void 0;
427
491
  if (!(fn.route ?? !(hasSchedule || hasAirway)) && !hasSchedule && !hasAirway) throw new Error(`oxy-app.json: function "${fnName}" must enable at least one of route/schedule/airwayStep`);
@@ -435,7 +499,7 @@ function isRecord(v) {
435
499
 
436
500
  //#endregion
437
501
  //#region src/customer-app/function-invoke.ts
438
- const inflight = /* @__PURE__ */ new Map();
502
+ const inflight$1 = /* @__PURE__ */ new Map();
439
503
  /**
440
504
  * Dedup key for an invocation: function name + its (stable-serialized) body,
441
505
  * joined by a newline. Function names are `[a-z][a-z0-9-]*` (no newline), so
@@ -450,12 +514,12 @@ function functionInvokeKey(name, body) {
450
514
  * this dedups concurrency only, it does NOT memoize the result.
451
515
  */
452
516
  function sharedFunctionInvoke(key, run) {
453
- const existing = inflight.get(key);
517
+ const existing = inflight$1.get(key);
454
518
  if (existing) return existing;
455
519
  const p = run().finally(() => {
456
- inflight.delete(key);
520
+ inflight$1.delete(key);
457
521
  });
458
- inflight.set(key, p);
522
+ inflight$1.set(key, p);
459
523
  return p;
460
524
  }
461
525
 
@@ -580,6 +644,50 @@ function isSafeLinkHref(raw) {
580
644
  return lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("mailto:");
581
645
  }
582
646
 
647
+ //#endregion
648
+ //#region src/customer-app/query-cache.ts
649
+ const SWR_TTL_MS = 3e4;
650
+ const inflight = /* @__PURE__ */ new Map();
651
+ const cache = /* @__PURE__ */ new Map();
652
+ function queryKey(projectId, db, sql) {
653
+ return `${projectId} ${db ?? ""} ${sql}`;
654
+ }
655
+ function getCached(projectId, sql, db) {
656
+ const e = cache.get(queryKey(projectId, db, sql));
657
+ return e && Date.now() - e.at < SWR_TTL_MS ? e.data : void 0;
658
+ }
659
+ /** Fetch with in-flight dedup + cache. `force` bypasses the fresh-cache
660
+ * short-circuit (used by refetch) but still dedupes a concurrent in-flight. */
661
+ async function sharedQuery(fetcher, projectId, sql, db, opts = {}) {
662
+ const key = queryKey(projectId, db, sql);
663
+ if (!opts.force) {
664
+ const fresh = getCached(projectId, sql, db);
665
+ if (fresh) return fresh;
666
+ }
667
+ const existing = inflight.get(key);
668
+ if (existing) return existing;
669
+ const body = JSON.stringify({
670
+ sql,
671
+ ...db ? { database: db } : {}
672
+ });
673
+ const p = (async () => {
674
+ const resp = await fetcher(`/api/projects/${projectId}/query`, {
675
+ method: "POST",
676
+ headers: { "content-type": "application/json" },
677
+ body
678
+ });
679
+ if (!resp.ok) throw await apiErrorFromResponse(resp);
680
+ const data = await resp.json();
681
+ cache.set(key, {
682
+ at: Date.now(),
683
+ data
684
+ });
685
+ return data;
686
+ })().finally(() => inflight.delete(key));
687
+ inflight.set(key, p);
688
+ return p;
689
+ }
690
+
583
691
  //#endregion
584
692
  //#region src/customer-app/react.tsx
585
693
  function defaultFetcher(input, init) {
@@ -684,58 +792,6 @@ function warnBetaOnce(name) {
684
792
  if (typeof console !== "undefined" && typeof console.warn === "function") console.warn(`[@oxy-hq/sdk] \`${name}\` is in beta — interface and behavior may change. See https://github.com/oxy-hq/customer-apps for caveats and the migration guide.`);
685
793
  }
686
794
  /**
687
- * Error thrown by all customer-app hooks when an API call returns a
688
- * non-2xx response. Carries the structured `code` + `hint` the server
689
- * emits so bundle UIs can render an actionable message instead of
690
- * "404: { ...json... }".
691
- *
692
- * The server contract is documented in
693
- * `crates/app/src/server/api/projects/agent_ask.rs` and
694
- * `procedure_run.rs` — both emit `{ message, code?, hint? }` as JSON.
695
- * Hooks that previously wrapped the raw text in `new Error()` now
696
- * throw this type instead.
697
- */
698
- var OxyApiError = class extends Error {
699
- constructor(opts) {
700
- const base = opts.message || `HTTP ${opts.status}`;
701
- const code = opts.code ? ` [${opts.code}]` : "";
702
- const hint = opts.hint ? `\n\n${opts.hint}` : "";
703
- super(`${base}${code}${hint}`);
704
- this.name = "OxyApiError";
705
- this.status = opts.status;
706
- this.code = opts.code ?? null;
707
- this.hint = opts.hint ?? null;
708
- }
709
- };
710
- /**
711
- * Read a non-2xx response from oxy and return an `OxyApiError`.
712
- * Parses the JSON envelope when present; falls back to raw text
713
- * (truncated to 240 chars so a runaway HTML error page doesn't
714
- * dominate the bundle UI).
715
- */
716
- async function apiErrorFromResponse(resp) {
717
- let body;
718
- let raw = "";
719
- try {
720
- raw = await resp.text();
721
- body = raw ? JSON.parse(raw) : void 0;
722
- } catch {}
723
- if (body && typeof body === "object") {
724
- const b = body;
725
- return new OxyApiError({
726
- status: resp.status,
727
- message: typeof b.message === "string" ? b.message : `HTTP ${resp.status}`,
728
- code: typeof b.code === "string" ? b.code : null,
729
- hint: typeof b.hint === "string" ? b.hint : null
730
- });
731
- }
732
- const snippet = raw.length > 240 ? `${raw.slice(0, 237)}…` : raw;
733
- return new OxyApiError({
734
- status: resp.status,
735
- message: snippet || `HTTP ${resp.status}`
736
- });
737
- }
738
- /**
739
795
  * Read the resolved manifest from context. Throws if called outside
740
796
  * `<OxyAppProvider>` — that's a programmer error worth surfacing
741
797
  * loudly, not silently swallowing.
@@ -789,36 +845,35 @@ function useQuery(input, opts = {}) {
789
845
  } : s);
790
846
  return;
791
847
  }
792
- const ctrl = new AbortController();
793
848
  let cancelled = false;
849
+ const cached = getCached(projectId, sqlWithParams, input.database);
850
+ if (cached && nonce === 0) {
851
+ const { columns, rows } = cached;
852
+ const objects = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
853
+ setState({
854
+ rows: objects,
855
+ columns,
856
+ loading: false,
857
+ error: null
858
+ });
859
+ return;
860
+ }
794
861
  setState((s) => ({
795
862
  ...s,
796
863
  loading: true,
797
864
  error: null
798
865
  }));
799
- const body = JSON.stringify({
800
- sql: sqlWithParams,
801
- ...input.database ? { database: input.database } : {}
802
- });
803
- fetcher(`/api/projects/${projectId}/query`, {
804
- method: "POST",
805
- headers: { "content-type": "application/json" },
806
- body,
807
- signal: ctrl.signal
808
- }).then(async (resp) => {
809
- if (!resp.ok) throw await apiErrorFromResponse(resp);
810
- return resp.json();
811
- }).then(({ columns, rows }) => {
866
+ sharedQuery(fetcher, projectId, sqlWithParams, input.database, { force: nonce > 0 }).then(({ columns, rows }) => {
812
867
  if (cancelled) return;
868
+ const objects = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
813
869
  setState({
814
- rows: rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]]))),
870
+ rows: objects,
815
871
  columns,
816
872
  loading: false,
817
873
  error: null
818
874
  });
819
875
  }).catch((err) => {
820
876
  if (cancelled) return;
821
- if (err instanceof DOMException && err.name === "AbortError") return;
822
877
  setState((s) => ({
823
878
  ...s,
824
879
  loading: false,
@@ -827,7 +882,6 @@ function useQuery(input, opts = {}) {
827
882
  });
828
883
  return () => {
829
884
  cancelled = true;
830
- ctrl.abort();
831
885
  };
832
886
  }, [
833
887
  enabled,
@@ -968,7 +1022,8 @@ function useSemanticQuery(input, opts = {}) {
968
1022
  filters: input.filters ?? [],
969
1023
  ...input.limit != null ? { limit: input.limit } : {}
970
1024
  });
971
- fetcher(`/api/projects/${projectId}/semantic-query${debug ? "?debug=1" : ""}`, {
1025
+ const url = `/api/projects/${projectId}/semantic-query${debug ? "?debug=1" : ""}`;
1026
+ fetcher(url, {
972
1027
  method: "POST",
973
1028
  headers: { "content-type": "application/json" },
974
1029
  body,
@@ -978,8 +1033,9 @@ function useSemanticQuery(input, opts = {}) {
978
1033
  return resp.json();
979
1034
  }).then(({ columns, rows, truncated, sql }) => {
980
1035
  if (cancelled) return;
1036
+ const objects = rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]])));
981
1037
  setState({
982
- rows: rows.map((r) => Object.fromEntries(columns.map((c, i) => [c, r[i]]))),
1038
+ rows: objects,
983
1039
  columns,
984
1040
  truncated,
985
1041
  sql: sql ?? null,
@@ -2313,6 +2369,7 @@ exports.OxyApiError = OxyApiError;
2313
2369
  exports.OxyAppProvider = OxyAppProvider;
2314
2370
  exports.OxyChat = OxyChat;
2315
2371
  exports._resetCustomerAppManifestCacheForTest = _resetCustomerAppManifestCacheForTest;
2372
+ exports.apiErrorFromResponse = apiErrorFromResponse;
2316
2373
  exports.getCustomerAppDebug = getCustomerAppDebug;
2317
2374
  exports.getOxyAppLogger = getOxyAppLogger;
2318
2375
  exports.interpretCustomerAppError = interpretCustomerAppError;