@pitcher/js-api 1.27.3 → 1.28.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/js-api.esm.js CHANGED
@@ -2511,6 +2511,251 @@ async function piaSearchAnswer(payload) {
2511
2511
  }
2512
2512
  }
2513
2513
 
2514
+ const APPS_DB_BASE_PATH = "/core/api/protected/appsdb";
2515
+ async function fetchAppsDbContext() {
2516
+ const env = await highLevelApi.API.request("get_env");
2517
+ const claimsDomain = env?.pitcher?.token_claims?.["https://pitcher.com/claims/urls"]?.custom_domain;
2518
+ const metadataDomain = env?.pitcher?.organization?.metadata?.custom_domain;
2519
+ let origin = claimsDomain || metadataDomain || "";
2520
+ if (origin && !origin.startsWith("http")) origin = `https://${origin}`;
2521
+ return {
2522
+ isIos: (env?.mode ?? env?.pitcher?.mode) === "IOS",
2523
+ origin,
2524
+ accessToken: env?.pitcher?.access_token ?? "",
2525
+ instanceId: env?.pitcher?.instance?.id
2526
+ };
2527
+ }
2528
+ const CONTEXT_TTL_MS = 6e4;
2529
+ let contextCache = null;
2530
+ function getAppsDbContext() {
2531
+ if (contextCache && Date.now() - contextCache.fetchedAt < CONTEXT_TTL_MS) {
2532
+ return contextCache.promise;
2533
+ }
2534
+ const promise = fetchAppsDbContext();
2535
+ contextCache = { promise, fetchedAt: Date.now() };
2536
+ promise.catch(() => {
2537
+ if (contextCache?.promise === promise) contextCache = null;
2538
+ });
2539
+ return promise;
2540
+ }
2541
+ async function appsDbRestFetch(ctx, path, init) {
2542
+ const base = ctx.origin || (typeof window !== "undefined" ? window.location.origin : "");
2543
+ const url = new URL(`${base}${APPS_DB_BASE_PATH}${path}`);
2544
+ Object.entries(init.params ?? {}).forEach(([key, value]) => {
2545
+ if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
2546
+ });
2547
+ const response = await fetch(url.toString(), {
2548
+ method: init.method,
2549
+ credentials: "include",
2550
+ headers: {
2551
+ "Content-Type": "application/json",
2552
+ Authorization: `Bearer ${ctx.accessToken}`,
2553
+ ...ctx.instanceId ? { "x-instance-id": ctx.instanceId } : {}
2554
+ },
2555
+ ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {}
2556
+ });
2557
+ if (!response.ok) {
2558
+ let errorBody;
2559
+ try {
2560
+ errorBody = await response.json();
2561
+ } catch {
2562
+ errorBody = void 0;
2563
+ }
2564
+ const error = new Error(
2565
+ `AppsDB request failed: ${response.status}${errorBody?.error ? ` — ${errorBody.error}` : ""}`
2566
+ );
2567
+ error.status = response.status;
2568
+ throw error;
2569
+ }
2570
+ if (response.status === 204 || response.status === 205) return void 0;
2571
+ const text = await response.text();
2572
+ return text ? JSON.parse(text) : void 0;
2573
+ }
2574
+ function hasErrorCode(error, code) {
2575
+ return error?.error_code === code || error?.errorCode === code || error?.code === code || typeof error?.reason === "string" && error.reason.includes(code) || typeof error?.message === "string" && error.message.includes(code);
2576
+ }
2577
+ function isTypeNotSyncedError(error) {
2578
+ return hasErrorCode(error, "type_not_synced");
2579
+ }
2580
+ function isBridgeUnsupportedError(error) {
2581
+ return hasErrorCode(error, "requestTypeDoesNotExists");
2582
+ }
2583
+ function parsePsqlLiteral(raw) {
2584
+ if (/^'.*'$/.test(raw)) return raw.slice(1, -1).replace(/''/g, "'");
2585
+ if (/^-?\d+(\.\d+)?$/.test(raw)) return Number(raw);
2586
+ if (/^true$/i.test(raw)) return true;
2587
+ if (/^false$/i.test(raw)) return false;
2588
+ if (/^null$/i.test(raw)) return null;
2589
+ return void 0;
2590
+ }
2591
+ function stripQuotedLiterals(where) {
2592
+ return where.replace(/'(?:[^']|'')*'/g, "''");
2593
+ }
2594
+ function splitTopLevelAnd(where) {
2595
+ const parts = [];
2596
+ let current = "";
2597
+ let inQuote = false;
2598
+ let i = 0;
2599
+ while (i < where.length) {
2600
+ const char = where[i];
2601
+ if (char === "'") {
2602
+ if (inQuote && where[i + 1] === "'") {
2603
+ current += "''";
2604
+ i += 2;
2605
+ continue;
2606
+ }
2607
+ inQuote = !inQuote;
2608
+ current += char;
2609
+ i += 1;
2610
+ continue;
2611
+ }
2612
+ if (!inQuote) {
2613
+ const separator = where.slice(i).match(/^ AND /i);
2614
+ if (separator) {
2615
+ parts.push(current);
2616
+ current = "";
2617
+ i += separator[0].length;
2618
+ continue;
2619
+ }
2620
+ }
2621
+ current += char;
2622
+ i += 1;
2623
+ }
2624
+ if (inQuote) return null;
2625
+ parts.push(current);
2626
+ return parts;
2627
+ }
2628
+ function parseSimplePsql(query) {
2629
+ const normalized = query.replace(/\s+/g, " ").trim();
2630
+ const match = normalized.match(/^SELECT \* FROM ([A-Za-z0-9_]+)( WHERE (.+))?$/i);
2631
+ if (!match) return null;
2632
+ const type = match[1];
2633
+ const where = match[3];
2634
+ if (!where) return { type, conditions: [] };
2635
+ if (/\bOR\b|\bNOT\b|\bLIKE\b|\bIN\b|[<>]|!=/i.test(stripQuotedLiterals(where))) return null;
2636
+ const rawConditions = splitTopLevelAnd(where);
2637
+ if (!rawConditions) return null;
2638
+ const conditions = [];
2639
+ for (const part of rawConditions) {
2640
+ const condition = part.trim().match(/^([A-Za-z0-9_.]+) ?= ?(.+)$/);
2641
+ if (!condition) return null;
2642
+ const value = parsePsqlLiteral(condition[2].trim());
2643
+ if (value === void 0) return null;
2644
+ conditions.push({ path: condition[1], value });
2645
+ }
2646
+ return { type, conditions };
2647
+ }
2648
+ function entryMatches(entry, conditions) {
2649
+ return conditions.every(({ path, value }) => {
2650
+ const actual = path.split(".").reduce((acc, key) => acc == null ? void 0 : acc[key], entry);
2651
+ if (value === null) return actual === null || actual === void 0;
2652
+ if (actual === null || actual === void 0) return false;
2653
+ return String(actual) === String(value);
2654
+ });
2655
+ }
2656
+ async function appsDbGetEntries(payload) {
2657
+ if (!payload?.type || typeof payload.type !== "string") {
2658
+ return Promise.reject(new Error("type is required and must be a non-empty string"));
2659
+ }
2660
+ const ctx = await getAppsDbContext();
2661
+ if (ctx.isIos) {
2662
+ try {
2663
+ return await highLevelApi.API.request("appsdb_get_entries", payload);
2664
+ } catch (error) {
2665
+ if (!isTypeNotSyncedError(error) && !isBridgeUnsupportedError(error)) throw error;
2666
+ }
2667
+ }
2668
+ return appsDbRestFetch(ctx, "", { method: "GET", params: payload });
2669
+ }
2670
+ async function appsDbUpsertEntry(payload) {
2671
+ if (!payload?.data || typeof payload.data !== "object") {
2672
+ return Promise.reject(new Error("data is required and must be an object"));
2673
+ }
2674
+ if (!payload.id && !payload.type) {
2675
+ return Promise.reject(new Error("type is required when creating an entry (no id provided)"));
2676
+ }
2677
+ const ctx = await getAppsDbContext();
2678
+ if (ctx.isIos) {
2679
+ try {
2680
+ return await highLevelApi.API.request("appsdb_upsert_entry", payload);
2681
+ } catch (error) {
2682
+ if (!isBridgeUnsupportedError(error)) throw error;
2683
+ }
2684
+ }
2685
+ if (payload.id) {
2686
+ return appsDbRestFetch(ctx, `/${encodeURIComponent(payload.id)}`, { method: "PUT", body: { data: payload.data } });
2687
+ }
2688
+ return appsDbRestFetch(ctx, "", { method: "POST", body: payload });
2689
+ }
2690
+ async function appsDbDeleteEntry(payload) {
2691
+ if (!payload?.id || typeof payload.id !== "string") {
2692
+ return Promise.reject(new Error("id is required and must be a non-empty string"));
2693
+ }
2694
+ const ctx = await getAppsDbContext();
2695
+ if (ctx.isIos) {
2696
+ try {
2697
+ return await highLevelApi.API.request("appsdb_delete_entry", payload);
2698
+ } catch (error) {
2699
+ if (!isBridgeUnsupportedError(error)) throw error;
2700
+ }
2701
+ }
2702
+ await appsDbRestFetch(ctx, `/${encodeURIComponent(payload.id)}`, { method: "DELETE" });
2703
+ }
2704
+ async function isDeviceOffline() {
2705
+ try {
2706
+ return Boolean(await highLevelApi.API.request("is_offline"));
2707
+ } catch {
2708
+ return false;
2709
+ }
2710
+ }
2711
+ const MIRROR_PAGE_LIMIT = 1e3;
2712
+ async function psqlFromMirror(parsed) {
2713
+ try {
2714
+ const entries = [];
2715
+ let hasMore = true;
2716
+ while (hasMore) {
2717
+ const result = await highLevelApi.API.request("appsdb_get_entries", {
2718
+ type: parsed.type,
2719
+ limit: MIRROR_PAGE_LIMIT,
2720
+ offset: entries.length
2721
+ });
2722
+ const page = result?.entries ?? [];
2723
+ entries.push(...page);
2724
+ hasMore = Boolean(result?.hasMore) && page.length > 0;
2725
+ }
2726
+ const matched = entries.filter((entry) => entryMatches(entry, parsed.conditions));
2727
+ return { entries: matched, totalCount: matched.length, hasMore: false };
2728
+ } catch (error) {
2729
+ if (isTypeNotSyncedError(error) || isBridgeUnsupportedError(error)) return null;
2730
+ throw error;
2731
+ }
2732
+ }
2733
+ async function appsDbPsql(payload) {
2734
+ if (!payload?.query || typeof payload.query !== "string") {
2735
+ return Promise.reject(new Error("query is required and must be a non-empty string"));
2736
+ }
2737
+ const ctx = await getAppsDbContext();
2738
+ if (ctx.isIos) {
2739
+ const parsed = parseSimplePsql(payload.query);
2740
+ let mirrorUnavailable = false;
2741
+ if (parsed && await isDeviceOffline()) {
2742
+ const local = await psqlFromMirror(parsed);
2743
+ if (local) return local;
2744
+ mirrorUnavailable = true;
2745
+ }
2746
+ try {
2747
+ return await appsDbRestFetch(ctx, "/psql", { method: "POST", body: payload });
2748
+ } catch (error) {
2749
+ if (parsed && !mirrorUnavailable && error instanceof TypeError) {
2750
+ const local = await psqlFromMirror(parsed);
2751
+ if (local) return local;
2752
+ }
2753
+ throw error;
2754
+ }
2755
+ }
2756
+ return appsDbRestFetch(ctx, "/psql", { method: "POST", body: payload });
2757
+ }
2758
+
2514
2759
  function open$2(payload = {}) {
2515
2760
  return this.API.request("open", payload);
2516
2761
  }
@@ -3519,6 +3764,10 @@ const modules = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
3519
3764
  __proto__: null,
3520
3765
  aiComplete,
3521
3766
  aiGetCapabilities,
3767
+ appsDbDeleteEntry,
3768
+ appsDbGetEntries,
3769
+ appsDbPsql,
3770
+ appsDbUpsertEntry,
3522
3771
  assignCanvasTheme,
3523
3772
  close: close$2,
3524
3773
  createCanvas: createCanvas$1,
@@ -4148,7 +4397,10 @@ function getTopPitcherWindow(current = window) {
4148
4397
  }
4149
4398
 
4150
4399
  const TRUNCATE_LENGTH_TRIGGER = 10;
4400
+ const RAW_RESPONSE_METHODS = ["appsdb_get_entries", "appsdb_upsert_entry", "appsdb_delete_entry"];
4151
4401
  const RAW_PAYLOAD_METHODS = [
4402
+ // AppsDB `data` is an opaque app-owned blob — snake-casing its keys would corrupt stored payloads
4403
+ "appsdb_upsert_entry",
4152
4404
  "crm_create",
4153
4405
  "crm_upsert",
4154
4406
  "crm_describe",
@@ -4210,7 +4462,7 @@ class LowLevelApi extends EventEmitter {
4210
4462
  this.options.logLevel === "debug" && console.log(`Callback ${id} response:`, res);
4211
4463
  if (res.response.status === "ok") {
4212
4464
  resolve(
4213
- this.options.casing === "camel" ? camelCaseKeys(res.response.body) : res.response.body
4465
+ this.options.casing === "camel" && !RAW_RESPONSE_METHODS.includes(type) ? camelCaseKeys(res.response.body) : res.response.body
4214
4466
  );
4215
4467
  if (type === "get_env") updateOnlineHandlersEnv(res.response.body);
4216
4468
  } else if (res.response.status === "error") {
@@ -4221,7 +4473,9 @@ class LowLevelApi extends EventEmitter {
4221
4473
  js_api_response: JSON.stringify(truncateObject(res, 3, TRUNCATE_LENGTH_TRIGGER))
4222
4474
  });
4223
4475
  }
4224
- reject(this.options.casing === "camel" ? camelCaseKeys(res.response.body) : res.response.body);
4476
+ reject(
4477
+ this.options.casing === "camel" && !RAW_RESPONSE_METHODS.includes(type) ? camelCaseKeys(res.response.body) : res.response.body
4478
+ );
4225
4479
  } else {
4226
4480
  throw new Error("unsupported response status");
4227
4481
  }