clivly 0.10.7 → 0.10.8

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/drizzle.cjs CHANGED
@@ -60,6 +60,7 @@ function fromDrizzleProjection(db, projection, opts) {
60
60
  const head = [`select ${selectItems.join(", ")}`, fromClause].join(" ");
61
61
  return {
62
62
  entity: opts.entity,
63
+ pushSource: projection.baseTable,
63
64
  cursorField: cursorToken,
64
65
  idField: "id",
65
66
  ...opts.objectType === void 0 ? {} : { objectType: opts.objectType },
@@ -89,6 +90,7 @@ function fromDrizzleProjectionByIdentity(client, projection, identityExpr, fromC
89
90
  const head = [`select ${selectItems.join(", ")}`, fromClause].join(" ");
90
91
  return {
91
92
  entity: opts.entity,
93
+ pushSource: projection.baseTable,
92
94
  cursorField: "id",
93
95
  idField: "id",
94
96
  ...opts.objectType === void 0 ? {} : { objectType: opts.objectType },
package/drizzle.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { R as ProjectionMapping, z as ProjectionRef } from "./entity-config-w8oMBWf8.cjs";
2
2
  import { n as buildUniqueConstraints, r as discoverFromDrizzle } from "./drizzle-SD8WCvYj.cjs";
3
- import { t as NamespaceIntrospector, v as SyncSource } from "./namespace-probe-lj_0bJrc.cjs";
3
+ import { t as NamespaceIntrospector, v as SyncSource } from "./namespace-probe-C3xMOqWE.cjs";
4
4
  import { Table } from "drizzle-orm";
5
5
  //#region ../sdk/src/drizzle-projection.d.ts
6
6
  /**
package/drizzle.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { R as ProjectionMapping, z as ProjectionRef } from "./entity-config-w8oMBWf8.mjs";
2
2
  import { n as buildUniqueConstraints, r as discoverFromDrizzle } from "./drizzle-axugRJ0u.mjs";
3
- import { t as NamespaceIntrospector, v as SyncSource } from "./namespace-probe-ouHKZas2.mjs";
3
+ import { t as NamespaceIntrospector, v as SyncSource } from "./namespace-probe-BmxtnOfc.mjs";
4
4
  import { Table } from "drizzle-orm";
5
5
  //#region ../sdk/src/drizzle-projection.d.ts
6
6
  /**
package/drizzle.mjs CHANGED
@@ -59,6 +59,7 @@ function fromDrizzleProjection(db, projection, opts) {
59
59
  const head = [`select ${selectItems.join(", ")}`, fromClause].join(" ");
60
60
  return {
61
61
  entity: opts.entity,
62
+ pushSource: projection.baseTable,
62
63
  cursorField: cursorToken,
63
64
  idField: "id",
64
65
  ...opts.objectType === void 0 ? {} : { objectType: opts.objectType },
@@ -88,6 +89,7 @@ function fromDrizzleProjectionByIdentity(client, projection, identityExpr, fromC
88
89
  const head = [`select ${selectItems.join(", ")}`, fromClause].join(" ");
89
90
  return {
90
91
  entity: opts.entity,
92
+ pushSource: projection.baseTable,
91
93
  cursorField: "id",
92
94
  idField: "id",
93
95
  ...opts.objectType === void 0 ? {} : { objectType: opts.objectType },
package/index.cjs CHANGED
@@ -3,7 +3,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_entity_config = require("./entity-config-C2MfIDL2.cjs");
4
4
  require("./mapping-score-CaB3F4Uu.cjs");
5
5
  const require_internal_entity_heuristics = require("./internal-entity-heuristics.cjs");
6
- const require_sdk = require("./sdk-DbvqXNSh.cjs");
6
+ const require_sdk = require("./sdk-BAHcnhm7.cjs");
7
7
  let node_child_process = require("node:child_process");
8
8
  let node_fs = require("node:fs");
9
9
  let node_module = require("node:module");
@@ -1953,14 +1953,19 @@ export async function register() {
1953
1953
  return;
1954
1954
  }
1955
1955
  try {
1956
- // Built at runtime so the bundler cannot statically trace it. Next
1957
- // compiles this file for BOTH runtimes, and the guard above only runs on
1958
- // the edge — it does not stop the edge BUILD from following a literal
1959
- // import into your config, and from there into your database driver. A
1960
- // node-postgres app fails that pass on \`require('path')\` and every route
1961
- // 500s before Clivly is ever reached.
1962
- const specifier = "${importPath}";
1963
- const { default: clivly } = await import(/* webpackIgnore: true */ specifier);
1956
+ // A plain import, deliberately. Next compiles this file for BOTH runtimes
1957
+ // and follows this into your config and on into your database driver, so a
1958
+ // Node-only driver breaks the EDGE build — the guard above is a runtime
1959
+ // check and cannot prevent that. The fix belongs in next.config, where
1960
+ // \`clivly init\` writes an edge-only \`config.externals\` entry; nothing here
1961
+ // ever executes on the edge, so the driver only has to resolve.
1962
+ //
1963
+ // Do NOT make this untraceable (a variable specifier, webpackIgnore).
1964
+ // That silences the build error and breaks the app in a worse way:
1965
+ // clivly.config is TypeScript, so an unbundled import cannot load it, and
1966
+ // the catch below turns that into a warning — the app boots, serves
1967
+ // traffic, and never checks in.
1968
+ const { default: clivly } = await import("${importPath}");
1964
1969
  await clivly.start();
1965
1970
  } catch (error) {
1966
1971
  console.warn("[clivly] could not check in:", error);
@@ -2013,6 +2018,46 @@ function nextNeedsInstrumentationFlag(versionRange) {
2013
2018
  }
2014
2019
  const NEXT_INSTRUMENTATION_FLAG_STEP = `Your Next.js version reads instrumentation.ts only when it is switched on. Add this to next.config, or Clivly will never be told your app started:
2015
2020
  experimental: { instrumentationHook: true }`;
2021
+ const NODE_ONLY_DRIVERS = /* @__PURE__ */ new Set([
2022
+ "pg",
2023
+ "mysql",
2024
+ "mysql2",
2025
+ "better-sqlite3",
2026
+ "sqlite3",
2027
+ "tedious",
2028
+ "oracledb",
2029
+ "ioredis"
2030
+ ]);
2031
+ /**
2032
+ * The Node-only database drivers a config's dependency list contains.
2033
+ *
2034
+ * Next compiles instrumentation.ts for the edge runtime whether or not the app
2035
+ * uses it, and follows the boot hook's config import into the driver. `pg`
2036
+ * pulls in `pgpass` → `split2` → `stream`, none of which resolve there, so the
2037
+ * build fails and every route 500s — before any Clivly code runs.
2038
+ *
2039
+ * `serverExternalPackages` does NOT fix this. It applies to the Node server
2040
+ * bundle only; the edge pass ignores it entirely, which is why adding package
2041
+ * after package to it never terminates. An edge-only `config.externals` is what
2042
+ * works, and it is safe precisely because nothing in that bundle ever executes
2043
+ * — the boot hook returns early unless NEXT_RUNTIME is "nodejs".
2044
+ */
2045
+ function nodeOnlyDrivers(dependencies) {
2046
+ if (!dependencies) return [];
2047
+ return Object.keys(dependencies).filter((name) => NODE_ONLY_DRIVERS.has(name)).sort();
2048
+ }
2049
+ /** The next.config step an app needs when its config reaches a Node driver. */
2050
+ function nextEdgeExternalsStep(drivers) {
2051
+ const list = drivers.map((d) => `"${d}"`).join(", ");
2052
+ return `Your database driver (${drivers.join(", ")}) cannot be bundled for Next's edge runtime, which compiles instrumentation.ts whether or not you use it. Without this, every route fails to build. Add to next.config:
2053
+ webpack: (config, { nextRuntime }) => {
2054
+ if (nextRuntime === "edge") {
2055
+ config.externals = [...(config.externals ?? []), ${list}];
2056
+ }
2057
+ return config;
2058
+ }
2059
+ Nothing runs on the edge — the boot hook exits early there — so the driver only has to resolve.`;
2060
+ }
2016
2061
  const REMIX_BOOT_STEP = `Start Clivly when your app boots. Add this near the top of app/entry.server.tsx:
2017
2062
  import clivly from "./clivly.config";
2018
2063
  clivly.start().catch((error) => console.warn("[clivly] could not check in:", error));`;
@@ -2563,21 +2608,32 @@ function planEnv(args) {
2563
2608
  action: envAction(addedKeys.length, existing.length)
2564
2609
  };
2565
2610
  }
2566
- function declaredNextVersion(args) {
2611
+ function hostManifest(args) {
2567
2612
  const raw = args.readFile((0, node_path.join)(args.cwd, "package.json"));
2568
- if (!raw) return;
2569
- let pkg = {};
2613
+ if (!raw) return {};
2570
2614
  try {
2571
- pkg = JSON.parse(raw);
2615
+ return JSON.parse(raw);
2572
2616
  } catch {
2573
- pkg = {};
2617
+ return {};
2574
2618
  }
2619
+ }
2620
+ function declaredNextVersion(args) {
2621
+ const pkg = hostManifest(args);
2575
2622
  return pkg.dependencies?.next ?? pkg.devDependencies?.next;
2576
2623
  }
2624
+ function declaredDependencies(args) {
2625
+ return hostManifest(args).dependencies;
2626
+ }
2627
+ /** The edge-externals step, or null when no Node-only driver is declared. */
2628
+ function edgeExternalsStepFor(dependencies) {
2629
+ const drivers = nodeOnlyDrivers(dependencies);
2630
+ return drivers.length > 0 ? nextEdgeExternalsStep(drivers) : null;
2631
+ }
2577
2632
  function planBootPatch(args) {
2578
2633
  if (bootHookPath(args.framework, args.layout) !== null) return {
2579
2634
  file: null,
2580
- manual: args.framework === "nextjs" && nextNeedsInstrumentationFlag(declaredNextVersion(args)) ? NEXT_INSTRUMENTATION_FLAG_STEP : null
2635
+ manual: args.framework === "nextjs" && nextNeedsInstrumentationFlag(declaredNextVersion(args)) ? NEXT_INSTRUMENTATION_FLAG_STEP : null,
2636
+ extra: args.framework === "nextjs" ? edgeExternalsStepFor(declaredDependencies(args)) : null
2581
2637
  };
2582
2638
  if (args.framework === "remix") return {
2583
2639
  file: null,
@@ -2623,7 +2679,7 @@ function planMutations(args) {
2623
2679
  return {
2624
2680
  files,
2625
2681
  env: planEnv(args),
2626
- manualSteps: boot.manual ? [boot.manual] : []
2682
+ manualSteps: [boot.manual, boot.extra ?? null].filter((step) => step !== null)
2627
2683
  };
2628
2684
  }
2629
2685
  function planWidgetMutations(args) {
@@ -4030,6 +4086,10 @@ function previewMutations(deps, mutations, options) {
4030
4086
  out(` ${mutations.env.action.padEnd(9)} .env\n`);
4031
4087
  for (const key of mutations.env.addedKeys) out(` + ${key}=\n`);
4032
4088
  }
4089
+ if (mutations.manualSteps.length > 0) {
4090
+ out("\nYou will also need to:\n");
4091
+ for (const step of mutations.manualSteps) out(`\n ! ${step}\n`);
4092
+ }
4033
4093
  out(options.diff ? "\nRe-run without --diff to apply; --force to overwrite the files marked conflict.\n" : "\nRe-run without --dry-run to apply.\n");
4034
4094
  }
4035
4095
  function printNextSteps(out, authComplete, loginRan, envFile, needsViteDotenv = false) {
package/index.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { t as CANONICAL_FIELDS } from "./entity-config-bC3DX9f1.mjs";
3
3
  import "./mapping-score-C68uAqGB.mjs";
4
4
  import { hasDefiningColumn, isFrameworkTable, matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./internal-entity-heuristics.mjs";
5
- import { s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-BTzaM8bF.mjs";
5
+ import { s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-C0DU5KDS.mjs";
6
6
  import { createRequire } from "node:module";
7
7
  import { spawn } from "node:child_process";
8
8
  import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, watch, writeFileSync } from "node:fs";
@@ -1952,14 +1952,19 @@ export async function register() {
1952
1952
  return;
1953
1953
  }
1954
1954
  try {
1955
- // Built at runtime so the bundler cannot statically trace it. Next
1956
- // compiles this file for BOTH runtimes, and the guard above only runs on
1957
- // the edge — it does not stop the edge BUILD from following a literal
1958
- // import into your config, and from there into your database driver. A
1959
- // node-postgres app fails that pass on \`require('path')\` and every route
1960
- // 500s before Clivly is ever reached.
1961
- const specifier = "${importPath}";
1962
- const { default: clivly } = await import(/* webpackIgnore: true */ specifier);
1955
+ // A plain import, deliberately. Next compiles this file for BOTH runtimes
1956
+ // and follows this into your config and on into your database driver, so a
1957
+ // Node-only driver breaks the EDGE build — the guard above is a runtime
1958
+ // check and cannot prevent that. The fix belongs in next.config, where
1959
+ // \`clivly init\` writes an edge-only \`config.externals\` entry; nothing here
1960
+ // ever executes on the edge, so the driver only has to resolve.
1961
+ //
1962
+ // Do NOT make this untraceable (a variable specifier, webpackIgnore).
1963
+ // That silences the build error and breaks the app in a worse way:
1964
+ // clivly.config is TypeScript, so an unbundled import cannot load it, and
1965
+ // the catch below turns that into a warning — the app boots, serves
1966
+ // traffic, and never checks in.
1967
+ const { default: clivly } = await import("${importPath}");
1963
1968
  await clivly.start();
1964
1969
  } catch (error) {
1965
1970
  console.warn("[clivly] could not check in:", error);
@@ -2012,6 +2017,46 @@ function nextNeedsInstrumentationFlag(versionRange) {
2012
2017
  }
2013
2018
  const NEXT_INSTRUMENTATION_FLAG_STEP = `Your Next.js version reads instrumentation.ts only when it is switched on. Add this to next.config, or Clivly will never be told your app started:
2014
2019
  experimental: { instrumentationHook: true }`;
2020
+ const NODE_ONLY_DRIVERS = /* @__PURE__ */ new Set([
2021
+ "pg",
2022
+ "mysql",
2023
+ "mysql2",
2024
+ "better-sqlite3",
2025
+ "sqlite3",
2026
+ "tedious",
2027
+ "oracledb",
2028
+ "ioredis"
2029
+ ]);
2030
+ /**
2031
+ * The Node-only database drivers a config's dependency list contains.
2032
+ *
2033
+ * Next compiles instrumentation.ts for the edge runtime whether or not the app
2034
+ * uses it, and follows the boot hook's config import into the driver. `pg`
2035
+ * pulls in `pgpass` → `split2` → `stream`, none of which resolve there, so the
2036
+ * build fails and every route 500s — before any Clivly code runs.
2037
+ *
2038
+ * `serverExternalPackages` does NOT fix this. It applies to the Node server
2039
+ * bundle only; the edge pass ignores it entirely, which is why adding package
2040
+ * after package to it never terminates. An edge-only `config.externals` is what
2041
+ * works, and it is safe precisely because nothing in that bundle ever executes
2042
+ * — the boot hook returns early unless NEXT_RUNTIME is "nodejs".
2043
+ */
2044
+ function nodeOnlyDrivers(dependencies) {
2045
+ if (!dependencies) return [];
2046
+ return Object.keys(dependencies).filter((name) => NODE_ONLY_DRIVERS.has(name)).sort();
2047
+ }
2048
+ /** The next.config step an app needs when its config reaches a Node driver. */
2049
+ function nextEdgeExternalsStep(drivers) {
2050
+ const list = drivers.map((d) => `"${d}"`).join(", ");
2051
+ return `Your database driver (${drivers.join(", ")}) cannot be bundled for Next's edge runtime, which compiles instrumentation.ts whether or not you use it. Without this, every route fails to build. Add to next.config:
2052
+ webpack: (config, { nextRuntime }) => {
2053
+ if (nextRuntime === "edge") {
2054
+ config.externals = [...(config.externals ?? []), ${list}];
2055
+ }
2056
+ return config;
2057
+ }
2058
+ Nothing runs on the edge — the boot hook exits early there — so the driver only has to resolve.`;
2059
+ }
2015
2060
  const REMIX_BOOT_STEP = `Start Clivly when your app boots. Add this near the top of app/entry.server.tsx:
2016
2061
  import clivly from "./clivly.config";
2017
2062
  clivly.start().catch((error) => console.warn("[clivly] could not check in:", error));`;
@@ -2562,21 +2607,32 @@ function planEnv(args) {
2562
2607
  action: envAction(addedKeys.length, existing.length)
2563
2608
  };
2564
2609
  }
2565
- function declaredNextVersion(args) {
2610
+ function hostManifest(args) {
2566
2611
  const raw = args.readFile(join(args.cwd, "package.json"));
2567
- if (!raw) return;
2568
- let pkg = {};
2612
+ if (!raw) return {};
2569
2613
  try {
2570
- pkg = JSON.parse(raw);
2614
+ return JSON.parse(raw);
2571
2615
  } catch {
2572
- pkg = {};
2616
+ return {};
2573
2617
  }
2618
+ }
2619
+ function declaredNextVersion(args) {
2620
+ const pkg = hostManifest(args);
2574
2621
  return pkg.dependencies?.next ?? pkg.devDependencies?.next;
2575
2622
  }
2623
+ function declaredDependencies(args) {
2624
+ return hostManifest(args).dependencies;
2625
+ }
2626
+ /** The edge-externals step, or null when no Node-only driver is declared. */
2627
+ function edgeExternalsStepFor(dependencies) {
2628
+ const drivers = nodeOnlyDrivers(dependencies);
2629
+ return drivers.length > 0 ? nextEdgeExternalsStep(drivers) : null;
2630
+ }
2576
2631
  function planBootPatch(args) {
2577
2632
  if (bootHookPath(args.framework, args.layout) !== null) return {
2578
2633
  file: null,
2579
- manual: args.framework === "nextjs" && nextNeedsInstrumentationFlag(declaredNextVersion(args)) ? NEXT_INSTRUMENTATION_FLAG_STEP : null
2634
+ manual: args.framework === "nextjs" && nextNeedsInstrumentationFlag(declaredNextVersion(args)) ? NEXT_INSTRUMENTATION_FLAG_STEP : null,
2635
+ extra: args.framework === "nextjs" ? edgeExternalsStepFor(declaredDependencies(args)) : null
2580
2636
  };
2581
2637
  if (args.framework === "remix") return {
2582
2638
  file: null,
@@ -2622,7 +2678,7 @@ function planMutations(args) {
2622
2678
  return {
2623
2679
  files,
2624
2680
  env: planEnv(args),
2625
- manualSteps: boot.manual ? [boot.manual] : []
2681
+ manualSteps: [boot.manual, boot.extra ?? null].filter((step) => step !== null)
2626
2682
  };
2627
2683
  }
2628
2684
  function planWidgetMutations(args) {
@@ -4029,6 +4085,10 @@ function previewMutations(deps, mutations, options) {
4029
4085
  out(` ${mutations.env.action.padEnd(9)} .env\n`);
4030
4086
  for (const key of mutations.env.addedKeys) out(` + ${key}=\n`);
4031
4087
  }
4088
+ if (mutations.manualSteps.length > 0) {
4089
+ out("\nYou will also need to:\n");
4090
+ for (const step of mutations.manualSteps) out(`\n ! ${step}\n`);
4091
+ }
4032
4092
  out(options.diff ? "\nRe-run without --diff to apply; --force to overwrite the files marked conflict.\n" : "\nRe-run without --dry-run to apply.\n");
4033
4093
  }
4034
4094
  function printNextSteps(out, authComplete, loginRan, envFile, needsViteDotenv = false) {
@@ -332,6 +332,22 @@ interface SyncSource {
332
332
  * absent from another, and read by the runtime.
333
333
  */
334
334
  objectType?: string;
335
+ /**
336
+ * The `source` label a push carries, when it differs from `entity`.
337
+ *
338
+ * Only a projection sets this, and only because a projection entity has no
339
+ * choice but to carry two names: its entity key doubles as the custom-object
340
+ * slug and must be kebab-case, while `projection.entityKey` is validated by
341
+ * `/^[a-z][a-z0-9_]*$/` and rejects hyphens. The server resolves that by
342
+ * storing the mapping's `sourceTable` as `projection.baseTable` — so a push
343
+ * labelled with the entity key matched no mapping and every row was dropped,
344
+ * while `push-schema` reported the mapping created and exited 0.
345
+ *
346
+ * Deliberately NOT folded into `entity`: that value is also the cursor-store
347
+ * key, so redefining it would silently reset the sync cursor of every
348
+ * existing projection and re-push its whole history.
349
+ */
350
+ pushSource?: string;
335
351
  /** Set by `fromDrizzle`; lets `doctor` introspect the backing table. */
336
352
  [DRIZZLE_META]?: DrizzleSourceMeta;
337
353
  }
@@ -332,6 +332,22 @@ interface SyncSource {
332
332
  * absent from another, and read by the runtime.
333
333
  */
334
334
  objectType?: string;
335
+ /**
336
+ * The `source` label a push carries, when it differs from `entity`.
337
+ *
338
+ * Only a projection sets this, and only because a projection entity has no
339
+ * choice but to carry two names: its entity key doubles as the custom-object
340
+ * slug and must be kebab-case, while `projection.entityKey` is validated by
341
+ * `/^[a-z][a-z0-9_]*$/` and rejects hyphens. The server resolves that by
342
+ * storing the mapping's `sourceTable` as `projection.baseTable` — so a push
343
+ * labelled with the entity key matched no mapping and every row was dropped,
344
+ * while `push-schema` reported the mapping created and exited 0.
345
+ *
346
+ * Deliberately NOT folded into `entity`: that value is also the cursor-store
347
+ * key, so redefining it would silently reset the sync cursor of every
348
+ * existing projection and re-push its whole history.
349
+ */
350
+ pushSource?: string;
335
351
  /** Set by `fromDrizzle`; lets `doctor` introspect the backing table. */
336
352
  [DRIZZLE_META]?: DrizzleSourceMeta;
337
353
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clivly",
3
- "version": "0.10.7",
3
+ "version": "0.10.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Clivly CLI — scaffold a CRM into your app (clivly init).",
@@ -77,6 +77,43 @@ function resolveEntitySource(config, entityKey) {
77
77
  function expectedSourceKey(concept) {
78
78
  return CONCEPT_KEYS[concept] ?? "custom";
79
79
  }
80
+ /**
81
+ * The `source` label a push must carry for the server to find its mapping.
82
+ *
83
+ * `entity` is the SDK's own name for a source — the cursor-store key, and what
84
+ * the developer wrote in `config.source`. It is NOT always what the server
85
+ * filed the mapping under: for a projection, `ensureSchema` stores
86
+ * `sourceTable` as `projection.baseTable`, because a projection entity's key
87
+ * doubles as a kebab-case object slug and cannot also be the snake_case
88
+ * `projection.entityKey`.
89
+ *
90
+ * Pushing under `entity` therefore dropped 100% of a projection's rows while
91
+ * every local signal said the setup was correct. One function so the three
92
+ * push sites cannot drift apart again.
93
+ */
94
+ function pushSourceOf(source) {
95
+ return source.pushSource ?? source.entity;
96
+ }
97
+ /**
98
+ * The push label for a custom source, resolved against the config.
99
+ *
100
+ * `fromDrizzleProjection` stamps `pushSource` itself, but a projection source
101
+ * can equally be hand-built — `fromDrizzle` with an overridden `fetchPage` is
102
+ * what the README's own example leads you to, and next-finance did exactly
103
+ * that. Such a source has no `pushSource`, so the label has to come from the
104
+ * one place that is true either way: the entity's declared
105
+ * `projection.baseTable`.
106
+ *
107
+ * Matched on the source's `entity`, which for a custom source is also the
108
+ * config entity key (that is what `resolveEntitySource` above pairs them by).
109
+ * Falls back to `pushSourceOf`, so a non-projection custom source and an
110
+ * already-stamped one both keep their current label.
111
+ */
112
+ function customPushSource(config, source) {
113
+ if (source.pushSource) return source.pushSource;
114
+ const entities = config.entities?.entities;
115
+ return (entities?.[source.entity] ?? Object.values(entities ?? {}).find((e) => e.source === source.entity))?.projection?.baseTable ?? source.entity;
116
+ }
80
117
  //#endregion
81
118
  //#region ../sdk/src/concept-coverage.ts
82
119
  const CHECK_NAME$1 = "Entity → source coverage";
@@ -1203,7 +1240,7 @@ async function pushRemoteProjections(args) {
1203
1240
  }
1204
1241
  try {
1205
1242
  const ok = await args.push(SYNC_CUSTOM_PATH$1, {
1206
- source: source.entity,
1243
+ source: pushSourceOf(source),
1207
1244
  rows: all,
1208
1245
  mode: "full",
1209
1246
  fetched: all.length,
@@ -1278,7 +1315,7 @@ async function syncSource(args) {
1278
1315
  if (rows.length === 0) break;
1279
1316
  const capped = maxRows === void 0 ? rows : rows.slice(0, maxRows);
1280
1317
  if (!await push(path, {
1281
- source: source.entity,
1318
+ source: pushSourceOf(source),
1282
1319
  rows: capped,
1283
1320
  mode: "delta",
1284
1321
  fetched: rows.length,
@@ -1322,7 +1359,7 @@ async function fullSyncSource(args) {
1322
1359
  ok: true
1323
1360
  };
1324
1361
  const ok = await push(path, {
1325
- source: source.entity,
1362
+ source: pushSourceOf(source),
1326
1363
  rows: all,
1327
1364
  mode: "full",
1328
1365
  fetched: all.length,
@@ -1922,7 +1959,10 @@ function createClivlySDK(config) {
1922
1959
  };
1923
1960
  const syncCustomSources = async (customSources, mode, runId) => {
1924
1961
  const failed = [];
1925
- for (const customSource of customSources ?? []) if (!await syncOne(customSource, SYNC_CUSTOM_PATH, mode, customSource.objectType, runId)) failed.push(customSource.entity);
1962
+ for (const customSource of customSources ?? []) {
1963
+ customSource.pushSource = customPushSource(config, customSource);
1964
+ if (!await syncOne(customSource, SYNC_CUSTOM_PATH, mode, customSource.objectType, runId)) failed.push(customSource.entity);
1965
+ }
1926
1966
  return failed;
1927
1967
  };
1928
1968
  const syncRemoteSources = async (sources, runId) => {
@@ -77,6 +77,43 @@ function resolveEntitySource(config, entityKey) {
77
77
  function expectedSourceKey(concept) {
78
78
  return CONCEPT_KEYS[concept] ?? "custom";
79
79
  }
80
+ /**
81
+ * The `source` label a push must carry for the server to find its mapping.
82
+ *
83
+ * `entity` is the SDK's own name for a source — the cursor-store key, and what
84
+ * the developer wrote in `config.source`. It is NOT always what the server
85
+ * filed the mapping under: for a projection, `ensureSchema` stores
86
+ * `sourceTable` as `projection.baseTable`, because a projection entity's key
87
+ * doubles as a kebab-case object slug and cannot also be the snake_case
88
+ * `projection.entityKey`.
89
+ *
90
+ * Pushing under `entity` therefore dropped 100% of a projection's rows while
91
+ * every local signal said the setup was correct. One function so the three
92
+ * push sites cannot drift apart again.
93
+ */
94
+ function pushSourceOf(source) {
95
+ return source.pushSource ?? source.entity;
96
+ }
97
+ /**
98
+ * The push label for a custom source, resolved against the config.
99
+ *
100
+ * `fromDrizzleProjection` stamps `pushSource` itself, but a projection source
101
+ * can equally be hand-built — `fromDrizzle` with an overridden `fetchPage` is
102
+ * what the README's own example leads you to, and next-finance did exactly
103
+ * that. Such a source has no `pushSource`, so the label has to come from the
104
+ * one place that is true either way: the entity's declared
105
+ * `projection.baseTable`.
106
+ *
107
+ * Matched on the source's `entity`, which for a custom source is also the
108
+ * config entity key (that is what `resolveEntitySource` above pairs them by).
109
+ * Falls back to `pushSourceOf`, so a non-projection custom source and an
110
+ * already-stamped one both keep their current label.
111
+ */
112
+ function customPushSource(config, source) {
113
+ if (source.pushSource) return source.pushSource;
114
+ const entities = config.entities?.entities;
115
+ return (entities?.[source.entity] ?? Object.values(entities ?? {}).find((e) => e.source === source.entity))?.projection?.baseTable ?? source.entity;
116
+ }
80
117
  //#endregion
81
118
  //#region ../sdk/src/concept-coverage.ts
82
119
  const CHECK_NAME$1 = "Entity → source coverage";
@@ -1203,7 +1240,7 @@ async function pushRemoteProjections(args) {
1203
1240
  }
1204
1241
  try {
1205
1242
  const ok = await args.push(SYNC_CUSTOM_PATH$1, {
1206
- source: source.entity,
1243
+ source: pushSourceOf(source),
1207
1244
  rows: all,
1208
1245
  mode: "full",
1209
1246
  fetched: all.length,
@@ -1278,7 +1315,7 @@ async function syncSource(args) {
1278
1315
  if (rows.length === 0) break;
1279
1316
  const capped = maxRows === void 0 ? rows : rows.slice(0, maxRows);
1280
1317
  if (!await push(path, {
1281
- source: source.entity,
1318
+ source: pushSourceOf(source),
1282
1319
  rows: capped,
1283
1320
  mode: "delta",
1284
1321
  fetched: rows.length,
@@ -1322,7 +1359,7 @@ async function fullSyncSource(args) {
1322
1359
  ok: true
1323
1360
  };
1324
1361
  const ok = await push(path, {
1325
- source: source.entity,
1362
+ source: pushSourceOf(source),
1326
1363
  rows: all,
1327
1364
  mode: "full",
1328
1365
  fetched: all.length,
@@ -1922,7 +1959,10 @@ function createClivlySDK(config) {
1922
1959
  };
1923
1960
  const syncCustomSources = async (customSources, mode, runId) => {
1924
1961
  const failed = [];
1925
- for (const customSource of customSources ?? []) if (!await syncOne(customSource, SYNC_CUSTOM_PATH, mode, customSource.objectType, runId)) failed.push(customSource.entity);
1962
+ for (const customSource of customSources ?? []) {
1963
+ customSource.pushSource = customPushSource(config, customSource);
1964
+ if (!await syncOne(customSource, SYNC_CUSTOM_PATH, mode, customSource.objectType, runId)) failed.push(customSource.entity);
1965
+ }
1926
1966
  return failed;
1927
1967
  };
1928
1968
  const syncRemoteSources = async (sources, runId) => {
package/sdk.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_entity_config = require("./entity-config-C2MfIDL2.cjs");
3
- const require_sdk = require("./sdk-DbvqXNSh.cjs");
3
+ const require_sdk = require("./sdk-BAHcnhm7.cjs");
4
4
  exports.ClivlyAuthError = require_sdk.ClivlyAuthError;
5
5
  exports.ClivlyError = require_sdk.ClivlyError;
6
6
  exports.ClivlyNetworkError = require_sdk.ClivlyNetworkError;
package/sdk.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { C as customObjectDeclarations, M as validateEntitiesConfig, a as ClivlyEntitiesConfig, f as CustomObjectDeclaration, l as ConfigValidationError, m as DiscoveredSchemaTable, u as ConfigValidationResult, w as defineClivlyConfig } from "./entity-config-w8oMBWf8.cjs";
2
- import { _ as RemoteProjectionsDescription, a as ClivlyConnectResult, c as ClivlyUser, d as DiscoveredTable, f as DoctorCheck, g as EnsuredObjectTypeResult, h as EnsuredMappingResult, i as ClivlyConnectReason, l as CreateChatSessionHandlerOptions, m as EnsureSchemaResult, n as ChatSessionRequestBody, o as ClivlySDK, p as DoctorReport, r as ChatWidgetVisitor, s as ClivlySDKConfig, u as CursorStore, y as SyncTriggerDescription } from "./namespace-probe-lj_0bJrc.cjs";
2
+ import { _ as RemoteProjectionsDescription, a as ClivlyConnectResult, c as ClivlyUser, d as DiscoveredTable, f as DoctorCheck, g as EnsuredObjectTypeResult, h as EnsuredMappingResult, i as ClivlyConnectReason, l as CreateChatSessionHandlerOptions, m as EnsureSchemaResult, n as ChatSessionRequestBody, o as ClivlySDK, p as DoctorReport, r as ChatWidgetVisitor, s as ClivlySDKConfig, u as CursorStore, y as SyncTriggerDescription } from "./namespace-probe-C3xMOqWE.cjs";
3
3
  //#region ../sdk/src/trigger-url.d.ts
4
4
  declare function isPlaceholderUrl(value: string): boolean;
5
5
  //#endregion
package/sdk.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { C as customObjectDeclarations, M as validateEntitiesConfig, a as ClivlyEntitiesConfig, f as CustomObjectDeclaration, l as ConfigValidationError, m as DiscoveredSchemaTable, u as ConfigValidationResult, w as defineClivlyConfig } from "./entity-config-w8oMBWf8.mjs";
2
- import { _ as RemoteProjectionsDescription, a as ClivlyConnectResult, c as ClivlyUser, d as DiscoveredTable, f as DoctorCheck, g as EnsuredObjectTypeResult, h as EnsuredMappingResult, i as ClivlyConnectReason, l as CreateChatSessionHandlerOptions, m as EnsureSchemaResult, n as ChatSessionRequestBody, o as ClivlySDK, p as DoctorReport, r as ChatWidgetVisitor, s as ClivlySDKConfig, u as CursorStore, y as SyncTriggerDescription } from "./namespace-probe-ouHKZas2.mjs";
2
+ import { _ as RemoteProjectionsDescription, a as ClivlyConnectResult, c as ClivlyUser, d as DiscoveredTable, f as DoctorCheck, g as EnsuredObjectTypeResult, h as EnsuredMappingResult, i as ClivlyConnectReason, l as CreateChatSessionHandlerOptions, m as EnsureSchemaResult, n as ChatSessionRequestBody, o as ClivlySDK, p as DoctorReport, r as ChatWidgetVisitor, s as ClivlySDKConfig, u as CursorStore, y as SyncTriggerDescription } from "./namespace-probe-BmxtnOfc.mjs";
3
3
  //#region ../sdk/src/trigger-url.d.ts
4
4
  declare function isPlaceholderUrl(value: string): boolean;
5
5
  //#endregion
package/sdk.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import { c as defineClivlyConfig, g as validateEntitiesConfig, s as customObjectDeclarations } from "./entity-config-bC3DX9f1.mjs";
2
- import { a as ClivlyRateLimitError, i as ClivlyNetworkError, n as ClivlyAuthError, o as createChatSessionHandler, r as ClivlyError, s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-BTzaM8bF.mjs";
2
+ import { a as ClivlyRateLimitError, i as ClivlyNetworkError, n as ClivlyAuthError, o as createChatSessionHandler, r as ClivlyError, s as isPlaceholderUrl, t as createClivlySDK } from "./sdk-C0DU5KDS.mjs";
3
3
  export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, isPlaceholderUrl, validateEntitiesConfig };
package/vite.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { o as ClivlySDK } from "./namespace-probe-lj_0bJrc.cjs";
1
+ import { o as ClivlySDK } from "./namespace-probe-C3xMOqWE.cjs";
2
2
  import { Plugin } from "vite";
3
3
  //#region ../sdk/src/vite.d.ts
4
4
  interface ClivlyViteOptions {
package/vite.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { o as ClivlySDK } from "./namespace-probe-ouHKZas2.mjs";
1
+ import { o as ClivlySDK } from "./namespace-probe-BmxtnOfc.mjs";
2
2
  import { Plugin } from "vite";
3
3
  //#region ../sdk/src/vite.d.ts
4
4
  interface ClivlyViteOptions {