@clivly/core 0.9.6 → 0.9.7

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/README.md CHANGED
@@ -168,6 +168,19 @@ Refer to the base table as `base` and each join by its `key`. Validation is
168
168
  `uniqueConstraints` is rejected rather than assumed safe, because identity
169
169
  uniqueness cannot be proven without them.
170
170
 
171
+ `validateProjection` is also the SDK's guard when it runs an object authored in
172
+ Clivly's dashboard: the definition arrives over the network, and the host
173
+ revalidates it **against the schema the host itself discovered** before any SQL
174
+ is compiled. Because `checkTablesExist` and `checkRefs` bound every table and
175
+ column reference to the schema array you pass in, that call is what limits a
176
+ remote definition to the namespace the developer handed to `discoverFromDrizzle`
177
+ — pass a different schema and you move that boundary.
178
+
179
+ Since `0.9.7`, **every** identity kind must be provably unique, not just
180
+ composite ones: a single-column identity is rejected unless a unique key covers
181
+ it. The identity is what the sync pages by and what decides which rows survive a
182
+ snapshot, so an unproven one can silently skip and then archive real records.
183
+
171
184
  ### Fan-out
172
185
 
173
186
  A join that does not cover a unique key of the joined table can match many rows
package/dist/drizzle.cjs CHANGED
@@ -92,7 +92,7 @@ function buildColumnsMeta(table) {
92
92
  * when you import this entry (`@clivly/core/drizzle`); the rest of `@clivly/core`
93
93
  * stays dependency-free.
94
94
  */
95
- function discoverFromDrizzle(schema) {
95
+ function discoverFromDrizzle(schema, options = {}) {
96
96
  const tables = [];
97
97
  for (const value of Object.values(schema)) {
98
98
  if (!(0, drizzle_orm.is)(value, drizzle_orm.Table)) continue;
@@ -105,8 +105,16 @@ function discoverFromDrizzle(schema) {
105
105
  ...uniqueConstraints === void 0 ? {} : { uniqueConstraints }
106
106
  });
107
107
  }
108
+ if (tables.length === 0 && !options.allowEmpty) throw new Error(`discoverFromDrizzle: found no Drizzle tables in the object it was given (${describeKeys(schema)}). Import the schema module's tables directly — \`import * as schema from "./db/schema"\` — rather than a barrel or a default export, which can collapse to \`{ default }\` under ESM/CJS interop. Pass { allowEmpty: true } if an empty schema is genuinely intended.`);
108
109
  return tables;
109
110
  }
111
+ /** The keys actually present, so the message names what was handed over. */
112
+ function describeKeys(schema) {
113
+ const keys = Object.keys(schema);
114
+ if (keys.length === 0) return "it was empty";
115
+ const shown = keys.slice(0, 5).join(", ");
116
+ return keys.length > 5 ? `keys: ${shown}, +${keys.length - 5} more` : `keys: ${shown}`;
117
+ }
110
118
  //#endregion
111
119
  exports.buildColumnsMeta = buildColumnsMeta;
112
120
  exports.buildUniqueConstraints = buildUniqueConstraints;
@@ -36,6 +36,8 @@ declare function buildColumnsMeta(table: Table): DiscoveredColumnMeta[];
36
36
  * when you import this entry (`@clivly/core/drizzle`); the rest of `@clivly/core`
37
37
  * stays dependency-free.
38
38
  */
39
- declare function discoverFromDrizzle(schema: Record<string, unknown>): DiscoveredSchemaTable[];
39
+ declare function discoverFromDrizzle(schema: Record<string, unknown>, options?: {
40
+ allowEmpty?: boolean;
41
+ }): DiscoveredSchemaTable[];
40
42
  //#endregion
41
43
  export { buildColumnsMeta, buildUniqueConstraints, discoverFromDrizzle };
@@ -36,6 +36,8 @@ declare function buildColumnsMeta(table: Table): DiscoveredColumnMeta[];
36
36
  * when you import this entry (`@clivly/core/drizzle`); the rest of `@clivly/core`
37
37
  * stays dependency-free.
38
38
  */
39
- declare function discoverFromDrizzle(schema: Record<string, unknown>): DiscoveredSchemaTable[];
39
+ declare function discoverFromDrizzle(schema: Record<string, unknown>, options?: {
40
+ allowEmpty?: boolean;
41
+ }): DiscoveredSchemaTable[];
40
42
  //#endregion
41
43
  export { buildColumnsMeta, buildUniqueConstraints, discoverFromDrizzle };
package/dist/drizzle.mjs CHANGED
@@ -91,7 +91,7 @@ function buildColumnsMeta(table) {
91
91
  * when you import this entry (`@clivly/core/drizzle`); the rest of `@clivly/core`
92
92
  * stays dependency-free.
93
93
  */
94
- function discoverFromDrizzle(schema) {
94
+ function discoverFromDrizzle(schema, options = {}) {
95
95
  const tables = [];
96
96
  for (const value of Object.values(schema)) {
97
97
  if (!is(value, Table)) continue;
@@ -104,7 +104,15 @@ function discoverFromDrizzle(schema) {
104
104
  ...uniqueConstraints === void 0 ? {} : { uniqueConstraints }
105
105
  });
106
106
  }
107
+ if (tables.length === 0 && !options.allowEmpty) throw new Error(`discoverFromDrizzle: found no Drizzle tables in the object it was given (${describeKeys(schema)}). Import the schema module's tables directly — \`import * as schema from "./db/schema"\` — rather than a barrel or a default export, which can collapse to \`{ default }\` under ESM/CJS interop. Pass { allowEmpty: true } if an empty schema is genuinely intended.`);
107
108
  return tables;
108
109
  }
110
+ /** The keys actually present, so the message names what was handed over. */
111
+ function describeKeys(schema) {
112
+ const keys = Object.keys(schema);
113
+ if (keys.length === 0) return "it was empty";
114
+ const shown = keys.slice(0, 5).join(", ");
115
+ return keys.length > 5 ? `keys: ${shown}, +${keys.length - 5} more` : `keys: ${shown}`;
116
+ }
109
117
  //#endregion
110
118
  export { buildColumnsMeta, buildUniqueConstraints, discoverFromDrizzle };
@@ -138,7 +138,7 @@ function checkTablesExist(errors, projection, resolver, base) {
138
138
  if (!hasRichMeta(table)) {
139
139
  errors.push({
140
140
  path,
141
- message: `table "${name}" has no key/column metadata — upgrade @clivly/sdk and re-run discovery before using a projection`
141
+ message: `table "${name}" was discovered without column types or unique keys, which a projection needs to prove its id is unique. Redeploy or restart the app so it re-reports its schema (or run \`clivly push-schema\`); if the table is declared by hand as \`{ name, columns }\`, use \`discoverFromDrizzle\` so keys and types come with it.`
142
142
  });
143
143
  ok = false;
144
144
  }
@@ -183,10 +183,27 @@ function checkRefs(errors, projection, resolver, base, basePk) {
183
183
  for (const [field, binding] of Object.entries(projection.fieldBindings)) checkRef(errors, resolver, `${base}.projection.fieldBindings.${field}`, binding.from);
184
184
  for (const ref of require_projection.identityRefs(projection.identity, basePk)) checkRef(errors, resolver, `${base}.projection.identity`, ref);
185
185
  }
186
+ /**
187
+ * Prove that a single-column identity (`column`, or `base_pk` with an
188
+ * explicit column) is actually unique.
189
+ *
190
+ * `coversUniqueKey` fails closed on absent metadata, same as every other
191
+ * check in this file: a column not provably covered by a unique key is
192
+ * rejected, never assumed safe.
193
+ */
194
+ function checkSingleColumnIdentity(errors, resolver, path, ref) {
195
+ const table = resolver.tableFor(ref.table);
196
+ if (!table) return;
197
+ if (!coversUniqueKey(table, [ref.column])) errors.push({
198
+ path,
199
+ message: `identity column "${ref.table}.${ref.column}" is not covered by a unique key of "${table.name}" — pick a column with a unique constraint, or use a composite identity that is`
200
+ });
201
+ }
186
202
  function checkIdentitySafety(errors, projection, resolver, base, baseTable) {
187
203
  const path = `${base}.projection.identity`;
188
204
  const pk = primaryKeyColumns(baseTable);
189
- if (projection.identity.kind === "base_pk" && !projection.identity.column) {
205
+ const { identity } = projection;
206
+ if (identity.kind === "base_pk" && !identity.column) {
190
207
  if (pk.length === 0) {
191
208
  errors.push({
192
209
  path,
@@ -201,9 +218,21 @@ function checkIdentitySafety(errors, projection, resolver, base, baseTable) {
201
218
  });
202
219
  return;
203
220
  }
221
+ return;
222
+ }
223
+ if (identity.kind === "base_pk" && identity.column) {
224
+ checkSingleColumnIdentity(errors, resolver, path, {
225
+ table: require_projection.BASE_REF,
226
+ column: identity.column
227
+ });
228
+ return;
229
+ }
230
+ if (identity.kind === "column") {
231
+ checkSingleColumnIdentity(errors, resolver, path, identity.from);
232
+ return;
204
233
  }
205
- if (projection.identity.kind !== "composite") return;
206
- for (const part of projection.identity.parts) {
234
+ if (identity.kind !== "composite") return;
235
+ for (const part of identity.parts) {
207
236
  const table = resolver.tableFor(part.table);
208
237
  const meta = table && columnMetaFor(table, part.column);
209
238
  if (!meta) continue;
@@ -138,7 +138,7 @@ function checkTablesExist(errors, projection, resolver, base) {
138
138
  if (!hasRichMeta(table)) {
139
139
  errors.push({
140
140
  path,
141
- message: `table "${name}" has no key/column metadata — upgrade @clivly/sdk and re-run discovery before using a projection`
141
+ message: `table "${name}" was discovered without column types or unique keys, which a projection needs to prove its id is unique. Redeploy or restart the app so it re-reports its schema (or run \`clivly push-schema\`); if the table is declared by hand as \`{ name, columns }\`, use \`discoverFromDrizzle\` so keys and types come with it.`
142
142
  });
143
143
  ok = false;
144
144
  }
@@ -183,10 +183,27 @@ function checkRefs(errors, projection, resolver, base, basePk) {
183
183
  for (const [field, binding] of Object.entries(projection.fieldBindings)) checkRef(errors, resolver, `${base}.projection.fieldBindings.${field}`, binding.from);
184
184
  for (const ref of identityRefs(projection.identity, basePk)) checkRef(errors, resolver, `${base}.projection.identity`, ref);
185
185
  }
186
+ /**
187
+ * Prove that a single-column identity (`column`, or `base_pk` with an
188
+ * explicit column) is actually unique.
189
+ *
190
+ * `coversUniqueKey` fails closed on absent metadata, same as every other
191
+ * check in this file: a column not provably covered by a unique key is
192
+ * rejected, never assumed safe.
193
+ */
194
+ function checkSingleColumnIdentity(errors, resolver, path, ref) {
195
+ const table = resolver.tableFor(ref.table);
196
+ if (!table) return;
197
+ if (!coversUniqueKey(table, [ref.column])) errors.push({
198
+ path,
199
+ message: `identity column "${ref.table}.${ref.column}" is not covered by a unique key of "${table.name}" — pick a column with a unique constraint, or use a composite identity that is`
200
+ });
201
+ }
186
202
  function checkIdentitySafety(errors, projection, resolver, base, baseTable) {
187
203
  const path = `${base}.projection.identity`;
188
204
  const pk = primaryKeyColumns(baseTable);
189
- if (projection.identity.kind === "base_pk" && !projection.identity.column) {
205
+ const { identity } = projection;
206
+ if (identity.kind === "base_pk" && !identity.column) {
190
207
  if (pk.length === 0) {
191
208
  errors.push({
192
209
  path,
@@ -201,9 +218,21 @@ function checkIdentitySafety(errors, projection, resolver, base, baseTable) {
201
218
  });
202
219
  return;
203
220
  }
221
+ return;
222
+ }
223
+ if (identity.kind === "base_pk" && identity.column) {
224
+ checkSingleColumnIdentity(errors, resolver, path, {
225
+ table: BASE_REF,
226
+ column: identity.column
227
+ });
228
+ return;
229
+ }
230
+ if (identity.kind === "column") {
231
+ checkSingleColumnIdentity(errors, resolver, path, identity.from);
232
+ return;
204
233
  }
205
- if (projection.identity.kind !== "composite") return;
206
- for (const part of projection.identity.parts) {
234
+ if (identity.kind !== "composite") return;
235
+ for (const part of identity.parts) {
207
236
  const table = resolver.tableFor(part.table);
208
237
  const meta = table && columnMetaFor(table, part.column);
209
238
  if (!meta) continue;
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_entity_config = require("./entity-config-BAaZOIID.cjs");
2
+ const require_entity_config = require("./entity-config-25v_udeY.cjs");
3
3
  exports.CANONICAL_FIELDS = require_entity_config.CANONICAL_FIELDS;
4
4
  exports.CRM_CONCEPTS = require_entity_config.CRM_CONCEPTS;
5
5
  exports.CUSTOM_CONCEPT = require_entity_config.CUSTOM_CONCEPT;
@@ -1,2 +1,2 @@
1
- import { a as MATERIALIZING_CONCEPTS, c as entitiesConfigSchema, d as relationshipSchema, f as relationshipsSchema, i as ClivlyConfigError, l as filterSchema, n as CRM_CONCEPTS, o as customObjectDeclarations, p as validateEntitiesConfig, r as CUSTOM_CONCEPT, s as defineClivlyConfig, t as CANONICAL_FIELDS, u as projectionFields } from "./entity-config-kusaRiDs.mjs";
1
+ import { a as MATERIALIZING_CONCEPTS, c as entitiesConfigSchema, d as relationshipSchema, f as relationshipsSchema, i as ClivlyConfigError, l as filterSchema, n as CRM_CONCEPTS, o as customObjectDeclarations, p as validateEntitiesConfig, r as CUSTOM_CONCEPT, s as defineClivlyConfig, t as CANONICAL_FIELDS, u as projectionFields } from "./entity-config-DwGXbUo_.mjs";
2
2
  export { CANONICAL_FIELDS, CRM_CONCEPTS, CUSTOM_CONCEPT, ClivlyConfigError, MATERIALIZING_CONCEPTS, customObjectDeclarations, defineClivlyConfig, entitiesConfigSchema, filterSchema, projectionFields, relationshipSchema, relationshipsSchema, validateEntitiesConfig };
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_entity_config = require("./entity-config-BAaZOIID.cjs");
2
+ const require_entity_config = require("./entity-config-25v_udeY.cjs");
3
3
  const require_mapping_score = require("./mapping-score.cjs");
4
4
  //#region src/entity-heuristics.ts
5
5
  const TARGET_CONCEPTS = [
@@ -1,4 +1,4 @@
1
- import { a as MATERIALIZING_CONCEPTS } from "./entity-config-kusaRiDs.mjs";
1
+ import { a as MATERIALIZING_CONCEPTS } from "./entity-config-DwGXbUo_.mjs";
2
2
  import { CONCEPT_TIERS, FIELD_TIERS, RELATIONSHIP_TIERS, W, isAmbiguous, isNumericLike, isTextLike, isTimestampLike, tierFor } from "./mapping-score.mjs";
3
3
  //#region src/entity-heuristics.ts
4
4
  const TARGET_CONCEPTS = [
package/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_auth_adapter = require("./auth-adapter.cjs");
3
3
  const require_projection = require("./projection-OJYCYmS_.cjs");
4
- const require_entity_config = require("./entity-config-BAaZOIID.cjs");
4
+ const require_entity_config = require("./entity-config-25v_udeY.cjs");
5
5
  const require_mapping_form = require("./mapping-form.cjs");
6
6
  const require_mappings_config = require("./mappings-config.cjs");
7
7
  const require_view_compiler = require("./view-compiler-D30HNRnA.cjs");
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { authCustom } from "./auth-adapter.mjs";
2
2
  import { a as projectionSchema, i as identityRefs, n as PROJECTION_KEY_MESSAGE, r as PROJECTION_KEY_PATTERN, t as BASE_REF } from "./projection-BXw_iiw5.mjs";
3
- import { _ as columnMetaFor, a as MATERIALIZING_CONCEPTS, b as primaryKeyColumns, c as entitiesConfigSchema, d as relationshipSchema, f as relationshipsSchema, g as joinFansOut, h as fanOutJoins, i as ClivlyConfigError, l as filterSchema, m as validateProjection, n as CRM_CONCEPTS, p as validateEntitiesConfig, s as defineClivlyConfig, t as CANONICAL_FIELDS, v as coversUniqueKey, y as hasRichMeta } from "./entity-config-kusaRiDs.mjs";
3
+ import { _ as columnMetaFor, a as MATERIALIZING_CONCEPTS, b as primaryKeyColumns, c as entitiesConfigSchema, d as relationshipSchema, f as relationshipsSchema, g as joinFansOut, h as fanOutJoins, i as ClivlyConfigError, l as filterSchema, m as validateProjection, n as CRM_CONCEPTS, p as validateEntitiesConfig, s as defineClivlyConfig, t as CANONICAL_FIELDS, v as coversUniqueKey, y as hasRichMeta } from "./entity-config-DwGXbUo_.mjs";
4
4
  import { buildFilterFromDiscriminator, buildRelationships, relationshipViaSignature } from "./mapping-form.mjs";
5
5
  import { mappingsToEntitiesConfig } from "./mappings-config.mjs";
6
6
  import { c as compileProjectionIdentity, d as compileProjectionSelect, f as quoteIdent, i as explainEntitiesConfig, l as compileProjectionJoins, n as compileEntityView, o as PREVIEW_MAX_ROWS, r as compileEntityViews, s as compileProjectionFrom, t as DEFAULT_VIEW_PREFIX, u as compileProjectionPreview } from "./view-compiler-u3GGRgnv.mjs";
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_entity_config = require("./entity-config-BAaZOIID.cjs");
2
+ const require_entity_config = require("./entity-config-25v_udeY.cjs");
3
3
  //#region src/mappings-config.ts
4
4
  const RESERVED_FIELD = "id";
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { t as CANONICAL_FIELDS } from "./entity-config-kusaRiDs.mjs";
1
+ import { t as CANONICAL_FIELDS } from "./entity-config-DwGXbUo_.mjs";
2
2
  //#region src/mappings-config.ts
3
3
  const RESERVED_FIELD = "id";
4
4
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clivly/core",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "ORM/auth-agnostic CRM adapter contracts and shared domain types for Clivly.",