clivly 0.9.3 → 0.9.4

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.
Files changed (59) hide show
  1. package/core-entity-config.cjs +1 -1
  2. package/core-entity-config.mjs +1 -1
  3. package/core-mappings-config.cjs +1 -1
  4. package/core-mappings-config.mjs +1 -1
  5. package/core-sync-engine.cjs +1 -1
  6. package/core-sync-engine.mjs +1 -1
  7. package/core-view-compiler.cjs +1 -1
  8. package/core-view-compiler.mjs +1 -1
  9. package/core.cjs +11 -6
  10. package/core.d.cts +2 -2
  11. package/core.d.mts +2 -2
  12. package/core.mjs +8 -7
  13. package/drizzle.cjs +74 -9
  14. package/drizzle.d.cts +83 -7
  15. package/drizzle.d.mts +83 -7
  16. package/drizzle.mjs +74 -10
  17. package/{entity-config-DtBsl2MQ.cjs → entity-config-7sGCk_nw.cjs} +1 -1
  18. package/{entity-config-kUHri9yA.mjs → entity-config-B74SPOUZ.mjs} +1 -1
  19. package/index.cjs +60 -18
  20. package/index.mjs +60 -18
  21. package/{internal-Df-PtOH_.mjs → internal-CykXpEGk.mjs} +2 -2
  22. package/{internal-CwnEG6Wl.cjs → internal-DqduZU3l.cjs} +2 -2
  23. package/internal-entity-config.cjs +1 -1
  24. package/internal-entity-config.mjs +1 -1
  25. package/internal-entity-heuristics.cjs +1 -1
  26. package/internal-entity-heuristics.mjs +1 -1
  27. package/internal-mappings-config.cjs +1 -1
  28. package/internal-mappings-config.mjs +1 -1
  29. package/{internal-CDY8g9Ic.d.mts → internal-o2M7FGlR.d.mts} +29 -1
  30. package/internal-sync-engine.cjs +1 -1
  31. package/internal-sync-engine.mjs +1 -1
  32. package/internal-view-compiler.cjs +1 -1
  33. package/internal-view-compiler.mjs +1 -1
  34. package/{internal-Da_dlcQ_.d.cts → internal-wCXSELDN.d.cts} +29 -1
  35. package/internal.cjs +11 -6
  36. package/internal.d.cts +2 -2
  37. package/internal.d.mts +2 -2
  38. package/internal.mjs +8 -7
  39. package/{mappings-config-B0RqFLxu.cjs → mappings-config-CLyEg0sf.cjs} +1 -1
  40. package/{mappings-config-B1b1HVBg.mjs → mappings-config-DQS_s4dg.mjs} +1 -1
  41. package/{namespace-probe-EYyxWZnf.d.cts → namespace-probe-BoGI18dQ.d.cts} +8 -0
  42. package/{namespace-probe-BmdGnYMn.d.mts → namespace-probe-Cn7voAd0.d.mts} +8 -0
  43. package/package.json +1 -1
  44. package/{projection-D7xmQP4S.mjs → projection-CSGJbZlZ.mjs} +25 -4
  45. package/{projection-C5244vGa.cjs → projection-Dz6_i1-w.cjs} +25 -4
  46. package/projection-sql-BgnfiGqF.mjs +65 -0
  47. package/projection-sql-Cl1qIn8m.cjs +94 -0
  48. package/{sdk-COLUHhJq.cjs → sdk-CM1UO-I7.cjs} +58 -15
  49. package/{sdk-CZNXBsgA.mjs → sdk-DYBDR939.mjs} +53 -16
  50. package/sdk.cjs +3 -2
  51. package/sdk.d.cts +5 -2
  52. package/sdk.d.mts +5 -2
  53. package/sdk.mjs +3 -3
  54. package/{sync-engine-abE94g6X.cjs → sync-engine-C4A5jgA4.cjs} +1 -1
  55. package/{sync-engine-SuxSzbZ4.mjs → sync-engine-Dt5QQSKO.mjs} +1 -1
  56. package/{view-compiler-Blu4OXJ7.cjs → view-compiler-B5nDal14.cjs} +21 -74
  57. package/{view-compiler-CQx8TRrW.mjs → view-compiler-CSYwEWf5.mjs} +2 -55
  58. package/vite.d.cts +1 -1
  59. package/vite.d.mts +1 -1
@@ -4220,8 +4220,29 @@ function superRefine(fn, params) {
4220
4220
  */
4221
4221
  /** The reserved ref name for the projection's base table. */
4222
4222
  const BASE_REF = "base";
4223
- /** Entity keys become view names (`crm_src_<entityKey>`), so keep them slugs. */
4224
- const PROJECTION_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
4223
+ /**
4224
+ * Entity keys become view names (`crm_src_<entityKey>`), so keep them slugs.
4225
+ *
4226
+ * Accepts hyphens as well as underscores, matching `OBJECT_TYPE_SLUG_PATTERN`
4227
+ * on the server. Those two were widened apart: the server learned hyphens
4228
+ * because the SDK's own `slugify` collapses non-alphanumerics to them and
4229
+ * `clivly status` reports that form as canonical — but this pattern was missed,
4230
+ * so `clivly status` would tell you to key an entity `client-accounts` and then
4231
+ * `defineClivlyConfig` would reject the config it had just asked for. Same bug,
4232
+ * opposite half of the product.
4233
+ *
4234
+ * Widening is the safe direction: every key that was valid before still is, so
4235
+ * nothing existing is renamed. Safe at all because neither use is ever a bare
4236
+ * SQL identifier — the view name is quoted (`view-compiler.ts`, `quoteIdent`),
4237
+ * and so is a join's `key` when it is emitted as an alias.
4238
+ */
4239
+ const PROJECTION_KEY_PATTERN = /^[a-z][a-z0-9_-]*$/;
4240
+ /**
4241
+ * Stated in prose because the default for a failed `.regex()` reports only the
4242
+ * pattern — the developer is told the config is wrong and handed a regex to
4243
+ * decode. Naming the rule is what makes the failure actionable.
4244
+ */
4245
+ const PROJECTION_KEY_MESSAGE = "must be lowercase letters, digits, \"-\" or \"_\", starting with a letter";
4225
4246
  const projectionRefSchema = strictObject({
4226
4247
  table: string().min(1),
4227
4248
  column: string().min(1)
@@ -4233,7 +4254,7 @@ const projectionJoinConditionSchema = strictObject({
4233
4254
  });
4234
4255
  const projectionJoinSchema = strictObject({
4235
4256
  /** SQL alias and ref target. Unique within the projection. */
4236
- key: string().regex(PROJECTION_KEY_PATTERN),
4257
+ key: string().regex(PROJECTION_KEY_PATTERN, { message: `join key ${PROJECTION_KEY_MESSAGE}` }),
4237
4258
  table: string().min(1),
4238
4259
  type: _enum(["inner", "left"]),
4239
4260
  on: array(projectionJoinConditionSchema).min(1)
@@ -4257,7 +4278,7 @@ const projectionIdentitySchema = union([
4257
4278
  const projectionSchema = strictObject({
4258
4279
  version: literal(1),
4259
4280
  kind: literal("projection"),
4260
- entityKey: string().regex(PROJECTION_KEY_PATTERN),
4281
+ entityKey: string().regex(PROJECTION_KEY_PATTERN, { message: `entityKey ${PROJECTION_KEY_MESSAGE}` }),
4261
4282
  baseTable: string().min(1),
4262
4283
  joins: array(projectionJoinSchema),
4263
4284
  fieldBindings: record(string(), projectionFieldBindingSchema).refine((b) => Object.keys(b).length > 0, { message: "at least one field must be bound" }),
@@ -4220,8 +4220,29 @@ function superRefine(fn, params) {
4220
4220
  */
4221
4221
  /** The reserved ref name for the projection's base table. */
4222
4222
  const BASE_REF = "base";
4223
- /** Entity keys become view names (`crm_src_<entityKey>`), so keep them slugs. */
4224
- const PROJECTION_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
4223
+ /**
4224
+ * Entity keys become view names (`crm_src_<entityKey>`), so keep them slugs.
4225
+ *
4226
+ * Accepts hyphens as well as underscores, matching `OBJECT_TYPE_SLUG_PATTERN`
4227
+ * on the server. Those two were widened apart: the server learned hyphens
4228
+ * because the SDK's own `slugify` collapses non-alphanumerics to them and
4229
+ * `clivly status` reports that form as canonical — but this pattern was missed,
4230
+ * so `clivly status` would tell you to key an entity `client-accounts` and then
4231
+ * `defineClivlyConfig` would reject the config it had just asked for. Same bug,
4232
+ * opposite half of the product.
4233
+ *
4234
+ * Widening is the safe direction: every key that was valid before still is, so
4235
+ * nothing existing is renamed. Safe at all because neither use is ever a bare
4236
+ * SQL identifier — the view name is quoted (`view-compiler.ts`, `quoteIdent`),
4237
+ * and so is a join's `key` when it is emitted as an alias.
4238
+ */
4239
+ const PROJECTION_KEY_PATTERN = /^[a-z][a-z0-9_-]*$/;
4240
+ /**
4241
+ * Stated in prose because the default for a failed `.regex()` reports only the
4242
+ * pattern — the developer is told the config is wrong and handed a regex to
4243
+ * decode. Naming the rule is what makes the failure actionable.
4244
+ */
4245
+ const PROJECTION_KEY_MESSAGE = "must be lowercase letters, digits, \"-\" or \"_\", starting with a letter";
4225
4246
  const projectionRefSchema = strictObject({
4226
4247
  table: string().min(1),
4227
4248
  column: string().min(1)
@@ -4233,7 +4254,7 @@ const projectionJoinConditionSchema = strictObject({
4233
4254
  });
4234
4255
  const projectionJoinSchema = strictObject({
4235
4256
  /** SQL alias and ref target. Unique within the projection. */
4236
- key: string().regex(PROJECTION_KEY_PATTERN),
4257
+ key: string().regex(PROJECTION_KEY_PATTERN, { message: `join key ${PROJECTION_KEY_MESSAGE}` }),
4237
4258
  table: string().min(1),
4238
4259
  type: _enum(["inner", "left"]),
4239
4260
  on: array(projectionJoinConditionSchema).min(1)
@@ -4257,7 +4278,7 @@ const projectionIdentitySchema = union([
4257
4278
  const projectionSchema = strictObject({
4258
4279
  version: literal(1),
4259
4280
  kind: literal("projection"),
4260
- entityKey: string().regex(PROJECTION_KEY_PATTERN),
4281
+ entityKey: string().regex(PROJECTION_KEY_PATTERN, { message: `entityKey ${PROJECTION_KEY_MESSAGE}` }),
4261
4282
  baseTable: string().min(1),
4262
4283
  joins: array(projectionJoinSchema),
4263
4284
  fieldBindings: record(string(), projectionFieldBindingSchema).refine((b) => Object.keys(b).length > 0, { message: "at least one field must be bound" }),
@@ -0,0 +1,65 @@
1
+ import { r as identityRefs, t as BASE_REF } from "./projection-CSGJbZlZ.mjs";
2
+ //#region ../core/src/projection-sql.ts
3
+ /**
4
+ * SQL generation for projection-backed entities.
5
+ *
6
+ * Base and joins are aliased by their ref key (`base`, then each join's `key`),
7
+ * which is what allows the same table to be joined twice and keeps generated
8
+ * SQL independent of host table names.
9
+ *
10
+ * Pure string generation — no DB access. Identifiers are escaped so output is
11
+ * deterministic and injection-safe.
12
+ */
13
+ /** Quote a single identifier part, escaping embedded double-quotes. */
14
+ function quoteIdent(name) {
15
+ return `"${name.replace(/"/g, "\"\"")}"`;
16
+ }
17
+ function refSql(ref) {
18
+ return `${quoteIdent(ref.table)}.${quoteIdent(ref.column)}`;
19
+ }
20
+ /**
21
+ * The identity expression. Composite identities concatenate their parts with
22
+ * `::`; validation guarantees every part is NOT NULL and non-text, which is
23
+ * what makes the concatenation collision-free.
24
+ *
25
+ * Exported unaliased because two paths need the same expression under different
26
+ * names: the view compiler aliases it `id`, and the SDK's projection push source
27
+ * also orders and keyset-paginates by it. Sharing one definition is what keeps
28
+ * a pushed row's `id` identical to the one the view would have produced.
29
+ */
30
+ function compileProjectionIdentity(projection, basePkColumn) {
31
+ const refs = identityRefs(projection.identity, basePkColumn);
32
+ if (projection.identity.kind === "composite") return `CONCAT_WS('::', ${refs.map((ref) => `${refSql(ref)}::text`).join(", ")})`;
33
+ const [ref] = refs;
34
+ if (!ref) throw new Error("projection identity resolved to no column");
35
+ return refSql(ref);
36
+ }
37
+ /** The identity expression, aliased as `id`. */
38
+ function identitySql(projection, basePkColumn) {
39
+ return `${compileProjectionIdentity(projection, basePkColumn)} AS ${quoteIdent("id")}`;
40
+ }
41
+ function compileProjectionSelect(projection, basePkColumn) {
42
+ const selectItems = [identitySql(projection, basePkColumn)];
43
+ const columns = ["id"];
44
+ for (const [field, binding] of Object.entries(projection.fieldBindings)) {
45
+ selectItems.push(`${refSql(binding.from)} AS ${quoteIdent(field)}`);
46
+ columns.push(field);
47
+ }
48
+ return {
49
+ columns,
50
+ selectItems
51
+ };
52
+ }
53
+ function compileProjectionJoins(projection) {
54
+ return projection.joins.map((join) => {
55
+ const keyword = join.type === "inner" ? "INNER JOIN" : "LEFT JOIN";
56
+ const on = join.on.map((c) => `${refSql(c.left)} = ${refSql(c.right)}`).join(" AND ");
57
+ return `${keyword} ${quoteIdent(join.table)} AS ${quoteIdent(join.key)} ON ${on}`;
58
+ });
59
+ }
60
+ /** `FROM <baseTable> AS "base"` — the alias every ref resolves against. */
61
+ function compileProjectionFrom(projection) {
62
+ return `FROM ${quoteIdent(projection.baseTable)} AS ${quoteIdent(BASE_REF)}`;
63
+ }
64
+ //#endregion
65
+ export { quoteIdent as a, compileProjectionSelect as i, compileProjectionIdentity as n, compileProjectionJoins as r, compileProjectionFrom as t };
@@ -0,0 +1,94 @@
1
+ const require_projection = require("./projection-Dz6_i1-w.cjs");
2
+ //#region ../core/src/projection-sql.ts
3
+ /**
4
+ * SQL generation for projection-backed entities.
5
+ *
6
+ * Base and joins are aliased by their ref key (`base`, then each join's `key`),
7
+ * which is what allows the same table to be joined twice and keeps generated
8
+ * SQL independent of host table names.
9
+ *
10
+ * Pure string generation — no DB access. Identifiers are escaped so output is
11
+ * deterministic and injection-safe.
12
+ */
13
+ /** Quote a single identifier part, escaping embedded double-quotes. */
14
+ function quoteIdent(name) {
15
+ return `"${name.replace(/"/g, "\"\"")}"`;
16
+ }
17
+ function refSql(ref) {
18
+ return `${quoteIdent(ref.table)}.${quoteIdent(ref.column)}`;
19
+ }
20
+ /**
21
+ * The identity expression. Composite identities concatenate their parts with
22
+ * `::`; validation guarantees every part is NOT NULL and non-text, which is
23
+ * what makes the concatenation collision-free.
24
+ *
25
+ * Exported unaliased because two paths need the same expression under different
26
+ * names: the view compiler aliases it `id`, and the SDK's projection push source
27
+ * also orders and keyset-paginates by it. Sharing one definition is what keeps
28
+ * a pushed row's `id` identical to the one the view would have produced.
29
+ */
30
+ function compileProjectionIdentity(projection, basePkColumn) {
31
+ const refs = require_projection.identityRefs(projection.identity, basePkColumn);
32
+ if (projection.identity.kind === "composite") return `CONCAT_WS('::', ${refs.map((ref) => `${refSql(ref)}::text`).join(", ")})`;
33
+ const [ref] = refs;
34
+ if (!ref) throw new Error("projection identity resolved to no column");
35
+ return refSql(ref);
36
+ }
37
+ /** The identity expression, aliased as `id`. */
38
+ function identitySql(projection, basePkColumn) {
39
+ return `${compileProjectionIdentity(projection, basePkColumn)} AS ${quoteIdent("id")}`;
40
+ }
41
+ function compileProjectionSelect(projection, basePkColumn) {
42
+ const selectItems = [identitySql(projection, basePkColumn)];
43
+ const columns = ["id"];
44
+ for (const [field, binding] of Object.entries(projection.fieldBindings)) {
45
+ selectItems.push(`${refSql(binding.from)} AS ${quoteIdent(field)}`);
46
+ columns.push(field);
47
+ }
48
+ return {
49
+ columns,
50
+ selectItems
51
+ };
52
+ }
53
+ function compileProjectionJoins(projection) {
54
+ return projection.joins.map((join) => {
55
+ const keyword = join.type === "inner" ? "INNER JOIN" : "LEFT JOIN";
56
+ const on = join.on.map((c) => `${refSql(c.left)} = ${refSql(c.right)}`).join(" AND ");
57
+ return `${keyword} ${quoteIdent(join.table)} AS ${quoteIdent(join.key)} ON ${on}`;
58
+ });
59
+ }
60
+ /** `FROM <baseTable> AS "base"` — the alias every ref resolves against. */
61
+ function compileProjectionFrom(projection) {
62
+ return `FROM ${quoteIdent(projection.baseTable)} AS ${quoteIdent(require_projection.BASE_REF)}`;
63
+ }
64
+ //#endregion
65
+ Object.defineProperty(exports, "compileProjectionFrom", {
66
+ enumerable: true,
67
+ get: function() {
68
+ return compileProjectionFrom;
69
+ }
70
+ });
71
+ Object.defineProperty(exports, "compileProjectionIdentity", {
72
+ enumerable: true,
73
+ get: function() {
74
+ return compileProjectionIdentity;
75
+ }
76
+ });
77
+ Object.defineProperty(exports, "compileProjectionJoins", {
78
+ enumerable: true,
79
+ get: function() {
80
+ return compileProjectionJoins;
81
+ }
82
+ });
83
+ Object.defineProperty(exports, "compileProjectionSelect", {
84
+ enumerable: true,
85
+ get: function() {
86
+ return compileProjectionSelect;
87
+ }
88
+ });
89
+ Object.defineProperty(exports, "quoteIdent", {
90
+ enumerable: true,
91
+ get: function() {
92
+ return quoteIdent;
93
+ }
94
+ });
@@ -1,5 +1,5 @@
1
- const require_entity_config = require("./entity-config-DtBsl2MQ.cjs");
2
- const require_mappings_config = require("./mappings-config-B0RqFLxu.cjs");
1
+ const require_entity_config = require("./entity-config-7sGCk_nw.cjs");
2
+ const require_mappings_config = require("./mappings-config-CLyEg0sf.cjs");
3
3
  const require_drizzle_meta = require("./drizzle-meta-Bb1JWYqf.cjs");
4
4
  //#region ../sdk/src/deployment-identity.ts
5
5
  const MAX_DEPLOYMENT_ID_LENGTH = 128;
@@ -123,11 +123,16 @@ function checkConceptCoverage(config) {
123
123
  }
124
124
  //#endregion
125
125
  //#region ../sdk/src/custom-slug.ts
126
+ const SLUG_PATTERN = /^[a-z][a-z0-9_-]*$/;
126
127
  const NON_ALNUM = /[^a-z0-9]+/g;
127
128
  const EDGE_HYPHENS = /(^-|-$)/g;
128
129
  function slugify(value) {
129
130
  return value.toLowerCase().trim().replace(NON_ALNUM, "-").replace(EDGE_HYPHENS, "");
130
131
  }
132
+ function suggestionFor(declared) {
133
+ const candidate = slugify(declared);
134
+ return candidate !== declared && SLUG_PATTERN.test(candidate) ? ` Did you mean "${candidate}"?` : "";
135
+ }
131
136
  function checkCustomObjectSlugs(config) {
132
137
  const custom = config.source?.custom ?? [];
133
138
  const declared = config.entities ? require_entity_config.customObjectDeclarations(config.entities) : [];
@@ -141,12 +146,11 @@ function checkCustomObjectSlugs(config) {
141
146
  const seen = /* @__PURE__ */ new Map();
142
147
  for (const source of custom) {
143
148
  const declared = source.objectType;
144
- const canonical = slugify(declared);
145
- if (canonical === "") problems.push(`"${source.entity}" has an empty objectType.`);
146
- else if (canonical !== declared) problems.push(`"${declared}" is not a slug — the dashboard would store it as "${canonical}".`);
147
- const prior = seen.get(canonical);
148
- if (prior && canonical !== "") problems.push(`duplicate slug "${canonical}" declared by "${prior}" and "${source.entity}".`);
149
- else seen.set(canonical, source.entity);
149
+ if (declared === "") problems.push(`"${source.entity}" has an empty objectType.`);
150
+ else if (!SLUG_PATTERN.test(declared)) problems.push(`"${declared}" is not a valid object type slug — use lowercase letters, digits, "-" or "_", starting with a letter.${suggestionFor(declared)}`);
151
+ const prior = seen.get(declared);
152
+ if (prior && declared !== "") problems.push(`duplicate slug "${declared}" declared by "${prior}" and "${source.entity}".`);
153
+ else seen.set(declared, source.entity);
150
154
  }
151
155
  if (problems.length > 0) return {
152
156
  name: "Custom object slugs",
@@ -253,10 +257,20 @@ async function runMappingDryRun(config, entityKey) {
253
257
  detail: `"${resolved.source.entity}" is empty — nothing to map yet.`
254
258
  };
255
259
  const missing = Object.entries(fields).filter(([, column]) => !(column in row)).map(([field, column]) => `${field} → ${column}`);
260
+ if (missing.length === 0) return {
261
+ name,
262
+ ok: true,
263
+ detail: `Mapped ${Object.keys(fields).length} field(s) from a real "${resolved.source.entity}" row.`
264
+ };
265
+ if (config.entities?.entities?.[entityKey]?.projection !== void 0 && missing.length === Object.keys(fields).length) return {
266
+ name,
267
+ ok: false,
268
+ detail: `Row from "${resolved.source.entity}" carries none of the projection's columns (${missing.join(", ")}) — the source performs no joins, so the projection is declared but never executed. Build it with \`fromDrizzleProjection\` from \`clivly/drizzle\`, which runs the joins and returns rows keyed by ref token.`
269
+ };
256
270
  return {
257
271
  name,
258
- ok: missing.length === 0,
259
- detail: missing.length === 0 ? `Mapped ${Object.keys(fields).length} field(s) from a real "${resolved.source.entity}" row.` : `Row from "${resolved.source.entity}" is missing mapped column(s): ${missing.join(", ")}.`
272
+ ok: false,
273
+ detail: `Row from "${resolved.source.entity}" is missing mapped column(s): ${missing.join(", ")}.`
260
274
  };
261
275
  }
262
276
  //#endregion
@@ -465,9 +479,27 @@ async function mappingDryRunChecks(config) {
465
479
  return checks;
466
480
  }
467
481
  /**
482
+ * Why a source has no `DRIZZLE_META`, without asserting more than we know.
483
+ *
484
+ * There are two ways to get here and they are indistinguishable at runtime: the
485
+ * source really was hand-written, or it came from `fromDrizzle` and a spread
486
+ * stripped the metadata, which is non-enumerable. The second is easy to fall
487
+ * into precisely where `objectType` is present — `source.custom` is typed
488
+ * `Array<SyncSource & { objectType: string }>`, so attaching that property with
489
+ * `{ ...fromDrizzle(...), objectType }` used to be the only way to satisfy it.
490
+ *
491
+ * Flatly calling such a source "hand-written" sent developers looking for one
492
+ * that does not exist. Where `objectType` is present, name the likelier cause
493
+ * and the fix instead of guessing wrong with confidence.
494
+ */
495
+ function missingMetaDetail(source) {
496
+ const skipped = "column check skipped";
497
+ if (source.objectType === void 0) return `"${source.entity}" is a hand-written source — ${skipped}.`;
498
+ return `"${source.entity}" has no drizzle metadata — ${skipped}. If it came from \`fromDrizzle\`, a spread such as \`{ ...fromDrizzle(...), objectType }\` dropped it; pass \`objectType\` to \`fromDrizzle\` instead.`;
499
+ }
500
+ /**
468
501
  * Confirm each drizzle source actually has the cursor and id columns the sync
469
- * pages on. Hand-written sources have no column metadata, so they are skipped
470
- * rather than failed.
502
+ * pages on. Sources with no column metadata are skipped rather than failed.
471
503
  */
472
504
  function cursorColumnChecks(sources) {
473
505
  const checks = [];
@@ -478,7 +510,7 @@ function cursorColumnChecks(sources) {
478
510
  name: `Cursor/id columns exist (${source.entity})`,
479
511
  ok: false,
480
512
  skipped: true,
481
- detail: `"${source.entity}" is a hand-written source — column check skipped.`
513
+ detail: missingMetaDetail(source)
482
514
  });
483
515
  continue;
484
516
  }
@@ -1117,11 +1149,16 @@ function observedTriggerUrl(req) {
1117
1149
  return new URL(`${proto}://${host}${requestUrl.pathname}`).toString();
1118
1150
  } catch {}
1119
1151
  }
1152
+ const PLACEHOLDER_BRACKETS = /[<>]/;
1153
+ function isPlaceholderUrl(value) {
1154
+ return PLACEHOLDER_BRACKETS.test(value);
1155
+ }
1120
1156
  /**
1121
1157
  * Decide which trigger URL to report, in precedence order:
1122
1158
  *
1123
1159
  * 1. `configuredUrl` — an explicit `syncTrigger.url`. Someone typed this on
1124
- * purpose, so it beats anything inferred.
1160
+ * purpose, so it beats anything inferred. An unedited placeholder is not
1161
+ * "on purpose" and is skipped.
1125
1162
  * 2. platform env origin + `configuredPath` — available at boot, before any
1126
1163
  * request has arrived. Needs the path because platform variables only give
1127
1164
  * an origin.
@@ -1131,7 +1168,7 @@ function observedTriggerUrl(req) {
1131
1168
  * doesn't need to know about it.
1132
1169
  */
1133
1170
  function resolveTriggerUrl(args) {
1134
- if (args.configuredUrl) return args.configuredUrl;
1171
+ if (args.configuredUrl && !isPlaceholderUrl(args.configuredUrl)) return args.configuredUrl;
1135
1172
  const origin = originFromPlatformEnv(args.env);
1136
1173
  if (origin && args.configuredPath) return `${origin}${args.configuredPath.startsWith("/") ? args.configuredPath : `/${args.configuredPath}`}`;
1137
1174
  return args.observedUrl;
@@ -1997,3 +2034,9 @@ Object.defineProperty(exports, "createClivlySDK", {
1997
2034
  return createClivlySDK;
1998
2035
  }
1999
2036
  });
2037
+ Object.defineProperty(exports, "isPlaceholderUrl", {
2038
+ enumerable: true,
2039
+ get: function() {
2040
+ return isPlaceholderUrl;
2041
+ }
2042
+ });
@@ -1,5 +1,5 @@
1
- import { o as customObjectDeclarations } from "./entity-config-kUHri9yA.mjs";
2
- import { t as entitiesToMappings } from "./mappings-config-B1b1HVBg.mjs";
1
+ import { o as customObjectDeclarations } from "./entity-config-B74SPOUZ.mjs";
2
+ import { t as entitiesToMappings } from "./mappings-config-DQS_s4dg.mjs";
3
3
  import { t as DRIZZLE_META } from "./drizzle-meta-D-5k3n0i.mjs";
4
4
  //#region ../sdk/src/deployment-identity.ts
5
5
  const MAX_DEPLOYMENT_ID_LENGTH = 128;
@@ -123,11 +123,16 @@ function checkConceptCoverage(config) {
123
123
  }
124
124
  //#endregion
125
125
  //#region ../sdk/src/custom-slug.ts
126
+ const SLUG_PATTERN = /^[a-z][a-z0-9_-]*$/;
126
127
  const NON_ALNUM = /[^a-z0-9]+/g;
127
128
  const EDGE_HYPHENS = /(^-|-$)/g;
128
129
  function slugify(value) {
129
130
  return value.toLowerCase().trim().replace(NON_ALNUM, "-").replace(EDGE_HYPHENS, "");
130
131
  }
132
+ function suggestionFor(declared) {
133
+ const candidate = slugify(declared);
134
+ return candidate !== declared && SLUG_PATTERN.test(candidate) ? ` Did you mean "${candidate}"?` : "";
135
+ }
131
136
  function checkCustomObjectSlugs(config) {
132
137
  const custom = config.source?.custom ?? [];
133
138
  const declared = config.entities ? customObjectDeclarations(config.entities) : [];
@@ -141,12 +146,11 @@ function checkCustomObjectSlugs(config) {
141
146
  const seen = /* @__PURE__ */ new Map();
142
147
  for (const source of custom) {
143
148
  const declared = source.objectType;
144
- const canonical = slugify(declared);
145
- if (canonical === "") problems.push(`"${source.entity}" has an empty objectType.`);
146
- else if (canonical !== declared) problems.push(`"${declared}" is not a slug — the dashboard would store it as "${canonical}".`);
147
- const prior = seen.get(canonical);
148
- if (prior && canonical !== "") problems.push(`duplicate slug "${canonical}" declared by "${prior}" and "${source.entity}".`);
149
- else seen.set(canonical, source.entity);
149
+ if (declared === "") problems.push(`"${source.entity}" has an empty objectType.`);
150
+ else if (!SLUG_PATTERN.test(declared)) problems.push(`"${declared}" is not a valid object type slug — use lowercase letters, digits, "-" or "_", starting with a letter.${suggestionFor(declared)}`);
151
+ const prior = seen.get(declared);
152
+ if (prior && declared !== "") problems.push(`duplicate slug "${declared}" declared by "${prior}" and "${source.entity}".`);
153
+ else seen.set(declared, source.entity);
150
154
  }
151
155
  if (problems.length > 0) return {
152
156
  name: "Custom object slugs",
@@ -253,10 +257,20 @@ async function runMappingDryRun(config, entityKey) {
253
257
  detail: `"${resolved.source.entity}" is empty — nothing to map yet.`
254
258
  };
255
259
  const missing = Object.entries(fields).filter(([, column]) => !(column in row)).map(([field, column]) => `${field} → ${column}`);
260
+ if (missing.length === 0) return {
261
+ name,
262
+ ok: true,
263
+ detail: `Mapped ${Object.keys(fields).length} field(s) from a real "${resolved.source.entity}" row.`
264
+ };
265
+ if (config.entities?.entities?.[entityKey]?.projection !== void 0 && missing.length === Object.keys(fields).length) return {
266
+ name,
267
+ ok: false,
268
+ detail: `Row from "${resolved.source.entity}" carries none of the projection's columns (${missing.join(", ")}) — the source performs no joins, so the projection is declared but never executed. Build it with \`fromDrizzleProjection\` from \`clivly/drizzle\`, which runs the joins and returns rows keyed by ref token.`
269
+ };
256
270
  return {
257
271
  name,
258
- ok: missing.length === 0,
259
- detail: missing.length === 0 ? `Mapped ${Object.keys(fields).length} field(s) from a real "${resolved.source.entity}" row.` : `Row from "${resolved.source.entity}" is missing mapped column(s): ${missing.join(", ")}.`
272
+ ok: false,
273
+ detail: `Row from "${resolved.source.entity}" is missing mapped column(s): ${missing.join(", ")}.`
260
274
  };
261
275
  }
262
276
  //#endregion
@@ -465,9 +479,27 @@ async function mappingDryRunChecks(config) {
465
479
  return checks;
466
480
  }
467
481
  /**
482
+ * Why a source has no `DRIZZLE_META`, without asserting more than we know.
483
+ *
484
+ * There are two ways to get here and they are indistinguishable at runtime: the
485
+ * source really was hand-written, or it came from `fromDrizzle` and a spread
486
+ * stripped the metadata, which is non-enumerable. The second is easy to fall
487
+ * into precisely where `objectType` is present — `source.custom` is typed
488
+ * `Array<SyncSource & { objectType: string }>`, so attaching that property with
489
+ * `{ ...fromDrizzle(...), objectType }` used to be the only way to satisfy it.
490
+ *
491
+ * Flatly calling such a source "hand-written" sent developers looking for one
492
+ * that does not exist. Where `objectType` is present, name the likelier cause
493
+ * and the fix instead of guessing wrong with confidence.
494
+ */
495
+ function missingMetaDetail(source) {
496
+ const skipped = "column check skipped";
497
+ if (source.objectType === void 0) return `"${source.entity}" is a hand-written source — ${skipped}.`;
498
+ return `"${source.entity}" has no drizzle metadata — ${skipped}. If it came from \`fromDrizzle\`, a spread such as \`{ ...fromDrizzle(...), objectType }\` dropped it; pass \`objectType\` to \`fromDrizzle\` instead.`;
499
+ }
500
+ /**
468
501
  * Confirm each drizzle source actually has the cursor and id columns the sync
469
- * pages on. Hand-written sources have no column metadata, so they are skipped
470
- * rather than failed.
502
+ * pages on. Sources with no column metadata are skipped rather than failed.
471
503
  */
472
504
  function cursorColumnChecks(sources) {
473
505
  const checks = [];
@@ -478,7 +510,7 @@ function cursorColumnChecks(sources) {
478
510
  name: `Cursor/id columns exist (${source.entity})`,
479
511
  ok: false,
480
512
  skipped: true,
481
- detail: `"${source.entity}" is a hand-written source — column check skipped.`
513
+ detail: missingMetaDetail(source)
482
514
  });
483
515
  continue;
484
516
  }
@@ -1117,11 +1149,16 @@ function observedTriggerUrl(req) {
1117
1149
  return new URL(`${proto}://${host}${requestUrl.pathname}`).toString();
1118
1150
  } catch {}
1119
1151
  }
1152
+ const PLACEHOLDER_BRACKETS = /[<>]/;
1153
+ function isPlaceholderUrl(value) {
1154
+ return PLACEHOLDER_BRACKETS.test(value);
1155
+ }
1120
1156
  /**
1121
1157
  * Decide which trigger URL to report, in precedence order:
1122
1158
  *
1123
1159
  * 1. `configuredUrl` — an explicit `syncTrigger.url`. Someone typed this on
1124
- * purpose, so it beats anything inferred.
1160
+ * purpose, so it beats anything inferred. An unedited placeholder is not
1161
+ * "on purpose" and is skipped.
1125
1162
  * 2. platform env origin + `configuredPath` — available at boot, before any
1126
1163
  * request has arrived. Needs the path because platform variables only give
1127
1164
  * an origin.
@@ -1131,7 +1168,7 @@ function observedTriggerUrl(req) {
1131
1168
  * doesn't need to know about it.
1132
1169
  */
1133
1170
  function resolveTriggerUrl(args) {
1134
- if (args.configuredUrl) return args.configuredUrl;
1171
+ if (args.configuredUrl && !isPlaceholderUrl(args.configuredUrl)) return args.configuredUrl;
1135
1172
  const origin = originFromPlatformEnv(args.env);
1136
1173
  if (origin && args.configuredPath) return `${origin}${args.configuredPath.startsWith("/") ? args.configuredPath : `/${args.configuredPath}`}`;
1137
1174
  return args.observedUrl;
@@ -1961,4 +1998,4 @@ function createClivlySDK(config) {
1961
1998
  };
1962
1999
  }
1963
2000
  //#endregion
1964
- export { ClivlyRateLimitError as a, ClivlyNetworkError as i, ClivlyAuthError as n, createChatSessionHandler as o, ClivlyError as r, createClivlySDK as t };
2001
+ export { ClivlyRateLimitError as a, ClivlyNetworkError as i, ClivlyAuthError as n, createChatSessionHandler as o, ClivlyError as r, isPlaceholderUrl as s, createClivlySDK as t };
package/sdk.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_entity_config = require("./entity-config-DtBsl2MQ.cjs");
3
- const require_sdk = require("./sdk-COLUHhJq.cjs");
2
+ const require_entity_config = require("./entity-config-7sGCk_nw.cjs");
3
+ const require_sdk = require("./sdk-CM1UO-I7.cjs");
4
4
  exports.ClivlyAuthError = require_sdk.ClivlyAuthError;
5
5
  exports.ClivlyError = require_sdk.ClivlyError;
6
6
  exports.ClivlyNetworkError = require_sdk.ClivlyNetworkError;
@@ -9,4 +9,5 @@ exports.createChatSessionHandler = require_sdk.createChatSessionHandler;
9
9
  exports.createClivlySDK = require_sdk.createClivlySDK;
10
10
  exports.customObjectDeclarations = require_entity_config.customObjectDeclarations;
11
11
  exports.defineClivlyConfig = require_entity_config.defineClivlyConfig;
12
+ exports.isPlaceholderUrl = require_sdk.isPlaceholderUrl;
12
13
  exports.validateEntitiesConfig = require_entity_config.validateEntitiesConfig;
package/sdk.d.cts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { C as defineClivlyConfig, S as customObjectDeclarations, a as ClivlyEntitiesConfig, f as CustomObjectDeclaration, k as validateEntitiesConfig, l as ConfigValidationError, m as DiscoveredSchemaTable, u as ConfigValidationResult } from "./entity-config-B2RA53nw.cjs";
2
- import { 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 } from "./namespace-probe-EYyxWZnf.cjs";
2
+ import { 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 } from "./namespace-probe-BoGI18dQ.cjs";
3
+ //#region ../sdk/src/trigger-url.d.ts
4
+ declare function isPlaceholderUrl(value: string): boolean;
5
+ //#endregion
3
6
  //#region ../sdk/src/chat-session.d.ts
4
7
  declare function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl, fetchImpl, resolveUser }: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
5
8
  //#endregion
@@ -33,4 +36,4 @@ declare class ClivlyNetworkError extends ClivlyError {}
33
36
  */
34
37
  declare function createClivlySDK(config: ClivlySDKConfig): ClivlySDK;
35
38
  //#endregion
36
- export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, type ClivlyEntitiesConfig, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type ConfigValidationError, type ConfigValidationResult, type CreateChatSessionHandlerOptions, type CursorStore, type CustomObjectDeclaration, type DiscoveredSchemaTable, type DiscoveredTable, type DoctorCheck, type DoctorReport, type EnsureSchemaResult, type EnsuredMappingResult, type EnsuredObjectTypeResult, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, validateEntitiesConfig };
39
+ export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, type ClivlyEntitiesConfig, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type ConfigValidationError, type ConfigValidationResult, type CreateChatSessionHandlerOptions, type CursorStore, type CustomObjectDeclaration, type DiscoveredSchemaTable, type DiscoveredTable, type DoctorCheck, type DoctorReport, type EnsureSchemaResult, type EnsuredMappingResult, type EnsuredObjectTypeResult, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, isPlaceholderUrl, validateEntitiesConfig };
package/sdk.d.mts CHANGED
@@ -1,5 +1,8 @@
1
1
  import { C as defineClivlyConfig, S as customObjectDeclarations, a as ClivlyEntitiesConfig, f as CustomObjectDeclaration, k as validateEntitiesConfig, l as ConfigValidationError, m as DiscoveredSchemaTable, u as ConfigValidationResult } from "./entity-config-B2RA53nw.mjs";
2
- import { 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 } from "./namespace-probe-BmdGnYMn.mjs";
2
+ import { 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 } from "./namespace-probe-Cn7voAd0.mjs";
3
+ //#region ../sdk/src/trigger-url.d.ts
4
+ declare function isPlaceholderUrl(value: string): boolean;
5
+ //#endregion
3
6
  //#region ../sdk/src/chat-session.d.ts
4
7
  declare function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl, fetchImpl, resolveUser }: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
5
8
  //#endregion
@@ -33,4 +36,4 @@ declare class ClivlyNetworkError extends ClivlyError {}
33
36
  */
34
37
  declare function createClivlySDK(config: ClivlySDKConfig): ClivlySDK;
35
38
  //#endregion
36
- export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, type ClivlyEntitiesConfig, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type ConfigValidationError, type ConfigValidationResult, type CreateChatSessionHandlerOptions, type CursorStore, type CustomObjectDeclaration, type DiscoveredSchemaTable, type DiscoveredTable, type DoctorCheck, type DoctorReport, type EnsureSchemaResult, type EnsuredMappingResult, type EnsuredObjectTypeResult, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, validateEntitiesConfig };
39
+ export { type ChatSessionRequestBody, type ChatWidgetVisitor, ClivlyAuthError, type ClivlyConnectReason, type ClivlyConnectResult, type ClivlyEntitiesConfig, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, type ClivlySDK, type ClivlySDKConfig, type ClivlyUser, type ConfigValidationError, type ConfigValidationResult, type CreateChatSessionHandlerOptions, type CursorStore, type CustomObjectDeclaration, type DiscoveredSchemaTable, type DiscoveredTable, type DoctorCheck, type DoctorReport, type EnsureSchemaResult, type EnsuredMappingResult, type EnsuredObjectTypeResult, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, isPlaceholderUrl, validateEntitiesConfig };
package/sdk.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { o as customObjectDeclarations, p as validateEntitiesConfig, s as defineClivlyConfig } from "./entity-config-kUHri9yA.mjs";
2
- import { a as ClivlyRateLimitError, i as ClivlyNetworkError, n as ClivlyAuthError, o as createChatSessionHandler, r as ClivlyError, t as createClivlySDK } from "./sdk-CZNXBsgA.mjs";
3
- export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, validateEntitiesConfig };
1
+ import { o as customObjectDeclarations, p as validateEntitiesConfig, s as defineClivlyConfig } from "./entity-config-B74SPOUZ.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-DYBDR939.mjs";
3
+ export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, isPlaceholderUrl, validateEntitiesConfig };
@@ -1,4 +1,4 @@
1
- const require_view_compiler = require("./view-compiler-Blu4OXJ7.cjs");
1
+ const require_view_compiler = require("./view-compiler-B5nDal14.cjs");
2
2
  //#region ../core/src/sync-engine.ts
3
3
  /**
4
4
  * Diff a view's rows against the current mirror population for one entity.
@@ -1,4 +1,4 @@
1
- import { r as compileEntityViews } from "./view-compiler-CQx8TRrW.mjs";
1
+ import { r as compileEntityViews } from "./view-compiler-CSYwEWf5.mjs";
2
2
  //#region ../core/src/sync-engine.ts
3
3
  /**
4
4
  * Diff a view's rows against the current mirror population for one entity.