@clivly/sdk 0.9.3 → 0.9.5

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
@@ -289,6 +289,12 @@ npx clivly status # the whole ladder
289
289
  npx clivly status --config ./apps/web/clivly.config.ts # in a monorepo
290
290
  ```
291
291
 
292
+ Its **Boot hook wired** step is the one to check when the app runs, throws
293
+ nothing, and the dashboard stays empty anyway — it confirms that something
294
+ actually calls `start()` on boot. On Next.js below 15 that is usually the
295
+ missing `experimental: { instrumentationHook: true }`, without which
296
+ `instrumentation.ts` is ignored and `start()` is never reached.
297
+
292
298
  Put your instance in `clivly.config.ts` (`export default createClivlySDK({...})`)
293
299
  so the CLI can find it. `ping`, `connect` and `doctor` remain as deprecated
294
300
  aliases running parts of the same ladder.
@@ -1,4 +1,4 @@
1
- import { c as CreateChatSessionHandlerOptions } from "./types-Cgwxd4h_.cjs";
1
+ import { c as CreateChatSessionHandlerOptions } from "./types-Dz1LQYvQ.cjs";
2
2
  //#region src/chat-session.d.ts
3
3
  declare function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl, fetchImpl, resolveUser }: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
4
4
  //#endregion
@@ -1,4 +1,4 @@
1
- import { c as CreateChatSessionHandlerOptions } from "./types-Cgwxd4h_.mjs";
1
+ import { c as CreateChatSessionHandlerOptions } from "./types-Dz1LQYvQ.mjs";
2
2
  //#region src/chat-session.d.ts
3
3
  declare function createChatSessionHandler({ allowedOrigins, apiKey, apiUrl, fetchImpl, resolveUser }: CreateChatSessionHandlerOptions): (request: Request) => Promise<Response>;
4
4
  //#endregion
package/dist/drizzle.cjs CHANGED
@@ -2,6 +2,76 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_drizzle_meta = require("./drizzle-meta-CdCs30q3.cjs");
3
3
  let _clivly_core_drizzle = require("@clivly/core/drizzle");
4
4
  let drizzle_orm = require("drizzle-orm");
5
+ let _clivly_core = require("@clivly/core");
6
+ //#region src/drizzle-projection.ts
7
+ /** `<ref>.<column>` — the token `fields` carries and cloud-ingest reads. */
8
+ function refToken(ref) {
9
+ return `${ref.table}.${ref.column}`;
10
+ }
11
+ function refSql(ref) {
12
+ return `${(0, _clivly_core.quoteIdent)(ref.table)}.${(0, _clivly_core.quoteIdent)(ref.column)}`;
13
+ }
14
+ function parseRef(value) {
15
+ if (typeof value !== "string") return value;
16
+ const dot = value.indexOf(".");
17
+ if (dot <= 0 || dot === value.length - 1) throw new Error(`fromDrizzleProjection: cursorField "${value}" must be a ref token like "base.updated_at".`);
18
+ return {
19
+ table: value.slice(0, dot),
20
+ column: value.slice(dot + 1)
21
+ };
22
+ }
23
+ /**
24
+ * Refs address tables symbolically — `base` or a join key — never by table name,
25
+ * which is what stops a projection reaching a table it did not declare. An
26
+ * unresolvable ref would otherwise compile to SQL referencing a missing alias
27
+ * and fail at the database with a message that never mentions the config.
28
+ */
29
+ function assertKnownRef(ref, known, context) {
30
+ if (!known.has(ref.table)) throw new Error(`fromDrizzleProjection: ${context} names unknown ref "${ref.table}" — expected "${[...known].join("\", \"")}".`);
31
+ }
32
+ function normalizeRows(result) {
33
+ if (Array.isArray(result)) return result;
34
+ const rows = result.rows;
35
+ if (Array.isArray(rows)) return rows;
36
+ throw new Error("fromDrizzleProjection: db.execute() returned neither an array of rows nor { rows }.");
37
+ }
38
+ function fromDrizzleProjection(db, projection, opts) {
39
+ const client = db;
40
+ const known = /* @__PURE__ */ new Set([_clivly_core.BASE_REF, ...projection.joins.map((j) => j.key)]);
41
+ if (projection.identity.kind === "base_pk" && projection.identity.column === void 0 && opts.basePkColumn === void 0) throw new Error(`fromDrizzleProjection: projection "${projection.entityKey}" has a base_pk identity with no column — pass { basePkColumn }.`);
42
+ const cursorRef = parseRef(opts.cursorField);
43
+ assertKnownRef(cursorRef, known, "cursorField");
44
+ for (const [field, binding] of Object.entries(projection.fieldBindings)) assertKnownRef(binding.from, known, `field "${field}"`);
45
+ const identityExpr = (0, _clivly_core.compileProjectionIdentity)(projection, opts.basePkColumn ?? "");
46
+ const cursorToken = refToken(cursorRef);
47
+ const cursorExpr = `CAST(${refSql(cursorRef)} AS timestamptz)`;
48
+ const cursorRead = `to_char(${cursorExpr} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`;
49
+ const selectItems = [`${identityExpr} AS ${(0, _clivly_core.quoteIdent)("id")}`, `${cursorRead} AS ${(0, _clivly_core.quoteIdent)(cursorToken)}`];
50
+ const emitted = /* @__PURE__ */ new Set([cursorToken]);
51
+ for (const binding of Object.values(projection.fieldBindings)) {
52
+ const token = refToken(binding.from);
53
+ if (emitted.has(token)) continue;
54
+ emitted.add(token);
55
+ selectItems.push(`${refSql(binding.from)} AS ${(0, _clivly_core.quoteIdent)(token)}`);
56
+ }
57
+ const head = [
58
+ `select ${selectItems.join(", ")}`,
59
+ (0, _clivly_core.compileProjectionFrom)(projection),
60
+ ...(0, _clivly_core.compileProjectionJoins)(projection)
61
+ ].join(" ");
62
+ return {
63
+ entity: opts.entity,
64
+ cursorField: cursorToken,
65
+ idField: "id",
66
+ ...opts.objectType === void 0 ? {} : { objectType: opts.objectType },
67
+ fetchPage: async ({ since, limit }) => {
68
+ const where = since ? drizzle_orm.sql` where (${drizzle_orm.sql.raw(cursorExpr)}, ${drizzle_orm.sql.raw(identityExpr)}) > (${since.time.toISOString()}::timestamptz, ${since.id})` : drizzle_orm.sql``;
69
+ const query = drizzle_orm.sql`${drizzle_orm.sql.raw(head)}${where} order by ${drizzle_orm.sql.raw(cursorExpr)} asc, ${drizzle_orm.sql.raw(identityExpr)} asc limit ${limit}`;
70
+ return normalizeRows(await client.execute(query));
71
+ }
72
+ };
73
+ }
74
+ //#endregion
5
75
  //#region src/drizzle.ts
6
76
  function inferIdField(table) {
7
77
  const columns = (0, drizzle_orm.getTableColumns)(table);
@@ -14,14 +84,6 @@ function inferIdField(table) {
14
84
  const kind = primaryKeys.length === 0 ? "no" : "a composite";
15
85
  throw new Error(`fromDrizzle: table "${(0, drizzle_orm.getTableName)(table)}" has ${kind} single-column primary key — pass { idField } explicitly.`);
16
86
  }
17
- /**
18
- * Build a `SyncSource` from a Drizzle table, generating the keyset-paginated
19
- * `fetchPage` the SDK needs. `cursorField`/`idField` are drizzle property keys;
20
- * `idField` defaults to the table's single-column primary key.
21
- *
22
- * `drizzle-orm` is an optional peer dependency — installed only when you import
23
- * `@clivly/sdk/drizzle`.
24
- */
25
87
  function fromDrizzle(db, table, opts) {
26
88
  const drizzleDb = db;
27
89
  const idField = opts.idField ?? inferIdField(table);
@@ -38,7 +100,8 @@ function fromDrizzle(db, table, opts) {
38
100
  entity: opts.entity,
39
101
  cursorField: cursorColumnName,
40
102
  idField: idColumnName,
41
- fetchPage: ({ since, limit }) => drizzleDb.select(projection).from(table).where(since ? drizzle_orm.sql`(${cursorColumn}, ${idColumn}) > (${since.time}, ${since.id})` : void 0).orderBy((0, drizzle_orm.asc)(cursorColumn), (0, drizzle_orm.asc)(idColumn)).limit(limit)
103
+ fetchPage: ({ since, limit }) => drizzleDb.select(projection).from(table).where(since ? drizzle_orm.sql`(${cursorColumn}, ${idColumn}) > (${since.time}, ${since.id})` : void 0).orderBy((0, drizzle_orm.asc)(cursorColumn), (0, drizzle_orm.asc)(idColumn)).limit(limit),
104
+ ...opts.objectType === void 0 ? {} : { objectType: opts.objectType }
42
105
  };
43
106
  const uniqueConstraints = (0, _clivly_core_drizzle.buildUniqueConstraints)(table);
44
107
  const meta = {
@@ -89,3 +152,4 @@ Object.defineProperty(exports, "discoverFromDrizzle", {
89
152
  });
90
153
  exports.drizzleIntrospector = drizzleIntrospector;
91
154
  exports.fromDrizzle = fromDrizzle;
155
+ exports.fromDrizzleProjection = fromDrizzleProjection;
@@ -1,23 +1,99 @@
1
- import { _ as NamespaceIntrospector, g as SyncSource } from "./types-Cgwxd4h_.cjs";
1
+ import { _ as NamespaceIntrospector, g as SyncSource } from "./types-Dz1LQYvQ.cjs";
2
+ import { ProjectionMapping, ProjectionRef } from "@clivly/core";
2
3
  import { Table } from "drizzle-orm";
3
4
  import { buildUniqueConstraints, discoverFromDrizzle } from "@clivly/core/drizzle";
5
+ //#region src/drizzle-projection.d.ts
6
+ /**
7
+ * Build a `SyncSource` that actually executes a projection's joins.
8
+ *
9
+ * `fromDrizzle` emits a single-table select, so a projection entity paired with
10
+ * it pushed raw base-table rows and every joined field mirrored empty — the
11
+ * declared joins were inert, and the only symptom was a dry run reporting a
12
+ * missing column whose name was correct.
13
+ *
14
+ * The row shape is the contract the cloud already relies on. A projection's
15
+ * `fields` are ref *tokens* (`client_name -> "cl.full_name"`, from
16
+ * `projectionFields`), and `cloud-ingest` reads `row[token]` verbatim plus
17
+ * `row.id` for `sourceRef`. So this aliases every binding by its token rather
18
+ * than by its field name.
19
+ *
20
+ * That is the one deliberate difference from the view compiler, which aliases
21
+ * by field name because the sync engine reads the compiled view directly. FROM,
22
+ * JOIN and the identity expression all come from `@clivly/core`, so both paths
23
+ * join identically and produce the same `id` for the same row.
24
+ *
25
+ * `drizzle-orm` is an optional peer dependency — installed only when you import
26
+ * `@clivly/sdk/drizzle`. The `db` only needs `.execute()`, the same surface
27
+ * `drizzleIntrospector` requires.
28
+ */
29
+ interface FromDrizzleProjectionOptions {
30
+ /**
31
+ * The base table's primary key, required only when `identity` is `base_pk`
32
+ * without an explicit column. Unlike `fromDrizzle` there is no `Table` object
33
+ * to infer it from — a projection names tables as strings.
34
+ */
35
+ basePkColumn?: string;
36
+ /**
37
+ * The column to keyset-paginate on, as a ref token (`"base.updated_at"`) or
38
+ * a `ProjectionRef`. A projection spans several tables, so there is no
39
+ * single table to infer a cursor from.
40
+ */
41
+ cursorField: string | ProjectionRef;
42
+ /** The source name rows are pushed under. */
43
+ entity: string;
44
+ /**
45
+ * The custom object slug. Accepted here so the result is directly assignable
46
+ * to `source.custom` — bolting it on with a spread afterwards drops the
47
+ * non-enumerable drizzle metadata.
48
+ */
49
+ objectType?: string;
50
+ }
51
+ declare function fromDrizzleProjection(db: unknown, projection: ProjectionMapping, opts: FromDrizzleProjectionOptions & {
52
+ objectType: string;
53
+ }): SyncSource & {
54
+ objectType: string;
55
+ };
56
+ declare function fromDrizzleProjection(db: unknown, projection: ProjectionMapping, opts: FromDrizzleProjectionOptions): SyncSource;
57
+ //#endregion
4
58
  //#region src/drizzle.d.ts
5
59
  interface DrizzleDb {
6
60
  select: (...args: any[]) => any;
7
61
  }
62
+ interface FromDrizzleOptions {
63
+ cursorField: string;
64
+ entity: string;
65
+ idField?: string;
66
+ /**
67
+ * Custom-object slug, for sources destined for `source.custom`.
68
+ *
69
+ * Pass it here rather than attaching it afterwards. `source.custom` is typed
70
+ * `Array<SyncSource & { objectType: string }>`, so before this option existed
71
+ * the only way to satisfy that type was to bolt the property on post-hoc —
72
+ * and the idiomatic spread (`{ ...fromDrizzle(...), objectType }`) copies only
73
+ * ENUMERABLE own properties, silently dropping the non-enumerable
74
+ * `DRIZZLE_META` below. `doctor` would then report a `fromDrizzle` source as
75
+ * "hand-written" and skip its cursor/id column checks, which is both a lost
76
+ * check and a false explanation.
77
+ */
78
+ objectType?: string;
79
+ }
8
80
  /**
9
81
  * Build a `SyncSource` from a Drizzle table, generating the keyset-paginated
10
82
  * `fetchPage` the SDK needs. `cursorField`/`idField` are drizzle property keys;
11
83
  * `idField` defaults to the table's single-column primary key.
12
84
  *
85
+ * Pass `objectType` for custom objects — the result is then directly assignable
86
+ * to `source.custom` with no spread or mutation.
87
+ *
13
88
  * `drizzle-orm` is an optional peer dependency — installed only when you import
14
89
  * `@clivly/sdk/drizzle`.
15
90
  */
16
- declare function fromDrizzle(db: DrizzleDb, table: Table, opts: {
17
- entity: string;
18
- cursorField: string;
19
- idField?: string;
20
- }): SyncSource;
91
+ declare function fromDrizzle(db: DrizzleDb, table: Table, opts: FromDrizzleOptions & {
92
+ objectType: string;
93
+ }): SyncSource & {
94
+ objectType: string;
95
+ };
96
+ declare function fromDrizzle(db: DrizzleDb, table: Table, opts: FromDrizzleOptions): SyncSource;
21
97
  /**
22
98
  * Build a `NamespaceIntrospector` from any Drizzle client that exposes
23
99
  * `.execute()`. Reads `information_schema` in the connection's default schema
@@ -25,4 +101,4 @@ declare function fromDrizzle(db: DrizzleDb, table: Table, opts: {
25
101
  */
26
102
  declare function drizzleIntrospector(db: unknown): NamespaceIntrospector;
27
103
  //#endregion
28
- export { buildUniqueConstraints, discoverFromDrizzle, drizzleIntrospector, fromDrizzle };
104
+ export { type FromDrizzleProjectionOptions, buildUniqueConstraints, discoverFromDrizzle, drizzleIntrospector, fromDrizzle, fromDrizzleProjection };
@@ -1,23 +1,99 @@
1
- import { _ as NamespaceIntrospector, g as SyncSource } from "./types-Cgwxd4h_.mjs";
1
+ import { _ as NamespaceIntrospector, g as SyncSource } from "./types-Dz1LQYvQ.mjs";
2
2
  import { buildUniqueConstraints, discoverFromDrizzle } from "@clivly/core/drizzle";
3
3
  import { Table } from "drizzle-orm";
4
+ import { ProjectionMapping, ProjectionRef } from "@clivly/core";
5
+ //#region src/drizzle-projection.d.ts
6
+ /**
7
+ * Build a `SyncSource` that actually executes a projection's joins.
8
+ *
9
+ * `fromDrizzle` emits a single-table select, so a projection entity paired with
10
+ * it pushed raw base-table rows and every joined field mirrored empty — the
11
+ * declared joins were inert, and the only symptom was a dry run reporting a
12
+ * missing column whose name was correct.
13
+ *
14
+ * The row shape is the contract the cloud already relies on. A projection's
15
+ * `fields` are ref *tokens* (`client_name -> "cl.full_name"`, from
16
+ * `projectionFields`), and `cloud-ingest` reads `row[token]` verbatim plus
17
+ * `row.id` for `sourceRef`. So this aliases every binding by its token rather
18
+ * than by its field name.
19
+ *
20
+ * That is the one deliberate difference from the view compiler, which aliases
21
+ * by field name because the sync engine reads the compiled view directly. FROM,
22
+ * JOIN and the identity expression all come from `@clivly/core`, so both paths
23
+ * join identically and produce the same `id` for the same row.
24
+ *
25
+ * `drizzle-orm` is an optional peer dependency — installed only when you import
26
+ * `@clivly/sdk/drizzle`. The `db` only needs `.execute()`, the same surface
27
+ * `drizzleIntrospector` requires.
28
+ */
29
+ interface FromDrizzleProjectionOptions {
30
+ /**
31
+ * The base table's primary key, required only when `identity` is `base_pk`
32
+ * without an explicit column. Unlike `fromDrizzle` there is no `Table` object
33
+ * to infer it from — a projection names tables as strings.
34
+ */
35
+ basePkColumn?: string;
36
+ /**
37
+ * The column to keyset-paginate on, as a ref token (`"base.updated_at"`) or
38
+ * a `ProjectionRef`. A projection spans several tables, so there is no
39
+ * single table to infer a cursor from.
40
+ */
41
+ cursorField: string | ProjectionRef;
42
+ /** The source name rows are pushed under. */
43
+ entity: string;
44
+ /**
45
+ * The custom object slug. Accepted here so the result is directly assignable
46
+ * to `source.custom` — bolting it on with a spread afterwards drops the
47
+ * non-enumerable drizzle metadata.
48
+ */
49
+ objectType?: string;
50
+ }
51
+ declare function fromDrizzleProjection(db: unknown, projection: ProjectionMapping, opts: FromDrizzleProjectionOptions & {
52
+ objectType: string;
53
+ }): SyncSource & {
54
+ objectType: string;
55
+ };
56
+ declare function fromDrizzleProjection(db: unknown, projection: ProjectionMapping, opts: FromDrizzleProjectionOptions): SyncSource;
57
+ //#endregion
4
58
  //#region src/drizzle.d.ts
5
59
  interface DrizzleDb {
6
60
  select: (...args: any[]) => any;
7
61
  }
62
+ interface FromDrizzleOptions {
63
+ cursorField: string;
64
+ entity: string;
65
+ idField?: string;
66
+ /**
67
+ * Custom-object slug, for sources destined for `source.custom`.
68
+ *
69
+ * Pass it here rather than attaching it afterwards. `source.custom` is typed
70
+ * `Array<SyncSource & { objectType: string }>`, so before this option existed
71
+ * the only way to satisfy that type was to bolt the property on post-hoc —
72
+ * and the idiomatic spread (`{ ...fromDrizzle(...), objectType }`) copies only
73
+ * ENUMERABLE own properties, silently dropping the non-enumerable
74
+ * `DRIZZLE_META` below. `doctor` would then report a `fromDrizzle` source as
75
+ * "hand-written" and skip its cursor/id column checks, which is both a lost
76
+ * check and a false explanation.
77
+ */
78
+ objectType?: string;
79
+ }
8
80
  /**
9
81
  * Build a `SyncSource` from a Drizzle table, generating the keyset-paginated
10
82
  * `fetchPage` the SDK needs. `cursorField`/`idField` are drizzle property keys;
11
83
  * `idField` defaults to the table's single-column primary key.
12
84
  *
85
+ * Pass `objectType` for custom objects — the result is then directly assignable
86
+ * to `source.custom` with no spread or mutation.
87
+ *
13
88
  * `drizzle-orm` is an optional peer dependency — installed only when you import
14
89
  * `@clivly/sdk/drizzle`.
15
90
  */
16
- declare function fromDrizzle(db: DrizzleDb, table: Table, opts: {
17
- entity: string;
18
- cursorField: string;
19
- idField?: string;
20
- }): SyncSource;
91
+ declare function fromDrizzle(db: DrizzleDb, table: Table, opts: FromDrizzleOptions & {
92
+ objectType: string;
93
+ }): SyncSource & {
94
+ objectType: string;
95
+ };
96
+ declare function fromDrizzle(db: DrizzleDb, table: Table, opts: FromDrizzleOptions): SyncSource;
21
97
  /**
22
98
  * Build a `NamespaceIntrospector` from any Drizzle client that exposes
23
99
  * `.execute()`. Reads `information_schema` in the connection's default schema
@@ -25,4 +101,4 @@ declare function fromDrizzle(db: DrizzleDb, table: Table, opts: {
25
101
  */
26
102
  declare function drizzleIntrospector(db: unknown): NamespaceIntrospector;
27
103
  //#endregion
28
- export { buildUniqueConstraints, discoverFromDrizzle, drizzleIntrospector, fromDrizzle };
104
+ export { type FromDrizzleProjectionOptions, buildUniqueConstraints, discoverFromDrizzle, drizzleIntrospector, fromDrizzle, fromDrizzleProjection };
package/dist/drizzle.mjs CHANGED
@@ -1,6 +1,76 @@
1
1
  import { t as DRIZZLE_META } from "./drizzle-meta-DNqzkREf.mjs";
2
2
  import { buildColumnsMeta, buildUniqueConstraints, buildUniqueConstraints as buildUniqueConstraints$1, discoverFromDrizzle } from "@clivly/core/drizzle";
3
3
  import { asc, getTableColumns, getTableName, sql } from "drizzle-orm";
4
+ import { BASE_REF, compileProjectionFrom, compileProjectionIdentity, compileProjectionJoins, quoteIdent } from "@clivly/core";
5
+ //#region src/drizzle-projection.ts
6
+ /** `<ref>.<column>` — the token `fields` carries and cloud-ingest reads. */
7
+ function refToken(ref) {
8
+ return `${ref.table}.${ref.column}`;
9
+ }
10
+ function refSql(ref) {
11
+ return `${quoteIdent(ref.table)}.${quoteIdent(ref.column)}`;
12
+ }
13
+ function parseRef(value) {
14
+ if (typeof value !== "string") return value;
15
+ const dot = value.indexOf(".");
16
+ if (dot <= 0 || dot === value.length - 1) throw new Error(`fromDrizzleProjection: cursorField "${value}" must be a ref token like "base.updated_at".`);
17
+ return {
18
+ table: value.slice(0, dot),
19
+ column: value.slice(dot + 1)
20
+ };
21
+ }
22
+ /**
23
+ * Refs address tables symbolically — `base` or a join key — never by table name,
24
+ * which is what stops a projection reaching a table it did not declare. An
25
+ * unresolvable ref would otherwise compile to SQL referencing a missing alias
26
+ * and fail at the database with a message that never mentions the config.
27
+ */
28
+ function assertKnownRef(ref, known, context) {
29
+ if (!known.has(ref.table)) throw new Error(`fromDrizzleProjection: ${context} names unknown ref "${ref.table}" — expected "${[...known].join("\", \"")}".`);
30
+ }
31
+ function normalizeRows(result) {
32
+ if (Array.isArray(result)) return result;
33
+ const rows = result.rows;
34
+ if (Array.isArray(rows)) return rows;
35
+ throw new Error("fromDrizzleProjection: db.execute() returned neither an array of rows nor { rows }.");
36
+ }
37
+ function fromDrizzleProjection(db, projection, opts) {
38
+ const client = db;
39
+ const known = /* @__PURE__ */ new Set([BASE_REF, ...projection.joins.map((j) => j.key)]);
40
+ if (projection.identity.kind === "base_pk" && projection.identity.column === void 0 && opts.basePkColumn === void 0) throw new Error(`fromDrizzleProjection: projection "${projection.entityKey}" has a base_pk identity with no column — pass { basePkColumn }.`);
41
+ const cursorRef = parseRef(opts.cursorField);
42
+ assertKnownRef(cursorRef, known, "cursorField");
43
+ for (const [field, binding] of Object.entries(projection.fieldBindings)) assertKnownRef(binding.from, known, `field "${field}"`);
44
+ const identityExpr = compileProjectionIdentity(projection, opts.basePkColumn ?? "");
45
+ const cursorToken = refToken(cursorRef);
46
+ const cursorExpr = `CAST(${refSql(cursorRef)} AS timestamptz)`;
47
+ const cursorRead = `to_char(${cursorExpr} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`;
48
+ const selectItems = [`${identityExpr} AS ${quoteIdent("id")}`, `${cursorRead} AS ${quoteIdent(cursorToken)}`];
49
+ const emitted = /* @__PURE__ */ new Set([cursorToken]);
50
+ for (const binding of Object.values(projection.fieldBindings)) {
51
+ const token = refToken(binding.from);
52
+ if (emitted.has(token)) continue;
53
+ emitted.add(token);
54
+ selectItems.push(`${refSql(binding.from)} AS ${quoteIdent(token)}`);
55
+ }
56
+ const head = [
57
+ `select ${selectItems.join(", ")}`,
58
+ compileProjectionFrom(projection),
59
+ ...compileProjectionJoins(projection)
60
+ ].join(" ");
61
+ return {
62
+ entity: opts.entity,
63
+ cursorField: cursorToken,
64
+ idField: "id",
65
+ ...opts.objectType === void 0 ? {} : { objectType: opts.objectType },
66
+ fetchPage: async ({ since, limit }) => {
67
+ const where = since ? sql` where (${sql.raw(cursorExpr)}, ${sql.raw(identityExpr)}) > (${since.time.toISOString()}::timestamptz, ${since.id})` : sql``;
68
+ const query = sql`${sql.raw(head)}${where} order by ${sql.raw(cursorExpr)} asc, ${sql.raw(identityExpr)} asc limit ${limit}`;
69
+ return normalizeRows(await client.execute(query));
70
+ }
71
+ };
72
+ }
73
+ //#endregion
4
74
  //#region src/drizzle.ts
5
75
  function inferIdField(table) {
6
76
  const columns = getTableColumns(table);
@@ -13,14 +83,6 @@ function inferIdField(table) {
13
83
  const kind = primaryKeys.length === 0 ? "no" : "a composite";
14
84
  throw new Error(`fromDrizzle: table "${getTableName(table)}" has ${kind} single-column primary key — pass { idField } explicitly.`);
15
85
  }
16
- /**
17
- * Build a `SyncSource` from a Drizzle table, generating the keyset-paginated
18
- * `fetchPage` the SDK needs. `cursorField`/`idField` are drizzle property keys;
19
- * `idField` defaults to the table's single-column primary key.
20
- *
21
- * `drizzle-orm` is an optional peer dependency — installed only when you import
22
- * `@clivly/sdk/drizzle`.
23
- */
24
86
  function fromDrizzle(db, table, opts) {
25
87
  const drizzleDb = db;
26
88
  const idField = opts.idField ?? inferIdField(table);
@@ -37,7 +99,8 @@ function fromDrizzle(db, table, opts) {
37
99
  entity: opts.entity,
38
100
  cursorField: cursorColumnName,
39
101
  idField: idColumnName,
40
- fetchPage: ({ since, limit }) => drizzleDb.select(projection).from(table).where(since ? sql`(${cursorColumn}, ${idColumn}) > (${since.time}, ${since.id})` : void 0).orderBy(asc(cursorColumn), asc(idColumn)).limit(limit)
102
+ fetchPage: ({ since, limit }) => drizzleDb.select(projection).from(table).where(since ? sql`(${cursorColumn}, ${idColumn}) > (${since.time}, ${since.id})` : void 0).orderBy(asc(cursorColumn), asc(idColumn)).limit(limit),
103
+ ...opts.objectType === void 0 ? {} : { objectType: opts.objectType }
41
104
  };
42
105
  const uniqueConstraints = buildUniqueConstraints$1(table);
43
106
  const meta = {
@@ -74,4 +137,4 @@ function drizzleIntrospector(db) {
74
137
  };
75
138
  }
76
139
  //#endregion
77
- export { buildUniqueConstraints, discoverFromDrizzle, drizzleIntrospector, fromDrizzle };
140
+ export { buildUniqueConstraints, discoverFromDrizzle, drizzleIntrospector, fromDrizzle, fromDrizzleProjection };
package/dist/index.cjs CHANGED
@@ -125,11 +125,16 @@ function checkConceptCoverage(config) {
125
125
  }
126
126
  //#endregion
127
127
  //#region src/custom-slug.ts
128
+ const SLUG_PATTERN = /^[a-z][a-z0-9_-]*$/;
128
129
  const NON_ALNUM = /[^a-z0-9]+/g;
129
130
  const EDGE_HYPHENS = /(^-|-$)/g;
130
131
  function slugify(value) {
131
132
  return value.toLowerCase().trim().replace(NON_ALNUM, "-").replace(EDGE_HYPHENS, "");
132
133
  }
134
+ function suggestionFor(declared) {
135
+ const candidate = slugify(declared);
136
+ return candidate !== declared && SLUG_PATTERN.test(candidate) ? ` Did you mean "${candidate}"?` : "";
137
+ }
133
138
  function checkCustomObjectSlugs(config) {
134
139
  const custom = config.source?.custom ?? [];
135
140
  const declared = config.entities ? (0, _clivly_core_entity_config.customObjectDeclarations)(config.entities) : [];
@@ -143,12 +148,11 @@ function checkCustomObjectSlugs(config) {
143
148
  const seen = /* @__PURE__ */ new Map();
144
149
  for (const source of custom) {
145
150
  const declared = source.objectType;
146
- const canonical = slugify(declared);
147
- if (canonical === "") problems.push(`"${source.entity}" has an empty objectType.`);
148
- else if (canonical !== declared) problems.push(`"${declared}" is not a slug — the dashboard would store it as "${canonical}".`);
149
- const prior = seen.get(canonical);
150
- if (prior && canonical !== "") problems.push(`duplicate slug "${canonical}" declared by "${prior}" and "${source.entity}".`);
151
- else seen.set(canonical, source.entity);
151
+ if (declared === "") problems.push(`"${source.entity}" has an empty objectType.`);
152
+ 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)}`);
153
+ const prior = seen.get(declared);
154
+ if (prior && declared !== "") problems.push(`duplicate slug "${declared}" declared by "${prior}" and "${source.entity}".`);
155
+ else seen.set(declared, source.entity);
152
156
  }
153
157
  if (problems.length > 0) return {
154
158
  name: "Custom object slugs",
@@ -255,10 +259,20 @@ async function runMappingDryRun(config, entityKey) {
255
259
  detail: `"${resolved.source.entity}" is empty — nothing to map yet.`
256
260
  };
257
261
  const missing = Object.entries(fields).filter(([, column]) => !(column in row)).map(([field, column]) => `${field} → ${column}`);
262
+ if (missing.length === 0) return {
263
+ name,
264
+ ok: true,
265
+ detail: `Mapped ${Object.keys(fields).length} field(s) from a real "${resolved.source.entity}" row.`
266
+ };
267
+ if (config.entities?.entities?.[entityKey]?.projection !== void 0 && missing.length === Object.keys(fields).length) return {
268
+ name,
269
+ ok: false,
270
+ 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.`
271
+ };
258
272
  return {
259
273
  name,
260
- ok: missing.length === 0,
261
- 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(", ")}.`
274
+ ok: false,
275
+ detail: `Row from "${resolved.source.entity}" is missing mapped column(s): ${missing.join(", ")}.`
262
276
  };
263
277
  }
264
278
  //#endregion
@@ -467,9 +481,27 @@ async function mappingDryRunChecks(config) {
467
481
  return checks;
468
482
  }
469
483
  /**
484
+ * Why a source has no `DRIZZLE_META`, without asserting more than we know.
485
+ *
486
+ * There are two ways to get here and they are indistinguishable at runtime: the
487
+ * source really was hand-written, or it came from `fromDrizzle` and a spread
488
+ * stripped the metadata, which is non-enumerable. The second is easy to fall
489
+ * into precisely where `objectType` is present — `source.custom` is typed
490
+ * `Array<SyncSource & { objectType: string }>`, so attaching that property with
491
+ * `{ ...fromDrizzle(...), objectType }` used to be the only way to satisfy it.
492
+ *
493
+ * Flatly calling such a source "hand-written" sent developers looking for one
494
+ * that does not exist. Where `objectType` is present, name the likelier cause
495
+ * and the fix instead of guessing wrong with confidence.
496
+ */
497
+ function missingMetaDetail(source) {
498
+ const skipped = "column check skipped";
499
+ if (source.objectType === void 0) return `"${source.entity}" is a hand-written source — ${skipped}.`;
500
+ 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.`;
501
+ }
502
+ /**
470
503
  * Confirm each drizzle source actually has the cursor and id columns the sync
471
- * pages on. Hand-written sources have no column metadata, so they are skipped
472
- * rather than failed.
504
+ * pages on. Sources with no column metadata are skipped rather than failed.
473
505
  */
474
506
  function cursorColumnChecks(sources) {
475
507
  const checks = [];
@@ -480,7 +512,7 @@ function cursorColumnChecks(sources) {
480
512
  name: `Cursor/id columns exist (${source.entity})`,
481
513
  ok: false,
482
514
  skipped: true,
483
- detail: `"${source.entity}" is a hand-written source — column check skipped.`
515
+ detail: missingMetaDetail(source)
484
516
  });
485
517
  continue;
486
518
  }
@@ -1119,11 +1151,16 @@ function observedTriggerUrl(req) {
1119
1151
  return new URL(`${proto}://${host}${requestUrl.pathname}`).toString();
1120
1152
  } catch {}
1121
1153
  }
1154
+ const PLACEHOLDER_BRACKETS = /[<>]/;
1155
+ function isPlaceholderUrl(value) {
1156
+ return PLACEHOLDER_BRACKETS.test(value);
1157
+ }
1122
1158
  /**
1123
1159
  * Decide which trigger URL to report, in precedence order:
1124
1160
  *
1125
1161
  * 1. `configuredUrl` — an explicit `syncTrigger.url`. Someone typed this on
1126
- * purpose, so it beats anything inferred.
1162
+ * purpose, so it beats anything inferred. An unedited placeholder is not
1163
+ * "on purpose" and is skipped.
1127
1164
  * 2. platform env origin + `configuredPath` — available at boot, before any
1128
1165
  * request has arrived. Needs the path because platform variables only give
1129
1166
  * an origin.
@@ -1133,7 +1170,7 @@ function observedTriggerUrl(req) {
1133
1170
  * doesn't need to know about it.
1134
1171
  */
1135
1172
  function resolveTriggerUrl(args) {
1136
- if (args.configuredUrl) return args.configuredUrl;
1173
+ if (args.configuredUrl && !isPlaceholderUrl(args.configuredUrl)) return args.configuredUrl;
1137
1174
  const origin = originFromPlatformEnv(args.env);
1138
1175
  if (origin && args.configuredPath) return `${origin}${args.configuredPath.startsWith("/") ? args.configuredPath : `/${args.configuredPath}`}`;
1139
1176
  return args.observedUrl;
@@ -1892,6 +1929,7 @@ Object.defineProperty(exports, "defineClivlyConfig", {
1892
1929
  return _clivly_core_entity_config.defineClivlyConfig;
1893
1930
  }
1894
1931
  });
1932
+ exports.isPlaceholderUrl = isPlaceholderUrl;
1895
1933
  Object.defineProperty(exports, "validateEntitiesConfig", {
1896
1934
  enumerable: true,
1897
1935
  get: function() {
package/dist/index.d.cts CHANGED
@@ -1,6 +1,9 @@
1
- import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, h as EnsuredObjectTypeResult, i as ClivlyConnectResult, l as CursorStore, m as EnsuredMappingResult, n as ChatWidgetVisitor, o as ClivlySDKConfig, p as EnsureSchemaResult, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable } from "./types-Cgwxd4h_.cjs";
1
+ import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, h as EnsuredObjectTypeResult, i as ClivlyConnectResult, l as CursorStore, m as EnsuredMappingResult, n as ChatWidgetVisitor, o as ClivlySDKConfig, p as EnsureSchemaResult, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable } from "./types-Dz1LQYvQ.cjs";
2
2
  import { createChatSessionHandler } from "./chat-session.cjs";
3
3
  import { ClivlyEntitiesConfig, ConfigValidationError, ConfigValidationResult, CustomObjectDeclaration, DiscoveredSchemaTable, customObjectDeclarations, defineClivlyConfig, validateEntitiesConfig } from "@clivly/core/entity-config";
4
+ //#region src/trigger-url.d.ts
5
+ declare function isPlaceholderUrl(value: string): boolean;
6
+ //#endregion
4
7
  //#region src/errors.d.ts
5
8
  declare class ClivlyError extends Error {
6
9
  /** HTTP status, or null when the request never completed (network error). */
@@ -31,4 +34,4 @@ declare class ClivlyNetworkError extends ClivlyError {}
31
34
  */
32
35
  declare function createClivlySDK(config: ClivlySDKConfig): ClivlySDK;
33
36
  //#endregion
34
- 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 };
37
+ 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/dist/index.d.mts CHANGED
@@ -1,6 +1,9 @@
1
- import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, h as EnsuredObjectTypeResult, i as ClivlyConnectResult, l as CursorStore, m as EnsuredMappingResult, n as ChatWidgetVisitor, o as ClivlySDKConfig, p as EnsureSchemaResult, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable } from "./types-Cgwxd4h_.mjs";
1
+ import { a as ClivlySDK, c as CreateChatSessionHandlerOptions, d as DoctorCheck, f as DoctorReport, h as EnsuredObjectTypeResult, i as ClivlyConnectResult, l as CursorStore, m as EnsuredMappingResult, n as ChatWidgetVisitor, o as ClivlySDKConfig, p as EnsureSchemaResult, r as ClivlyConnectReason, s as ClivlyUser, t as ChatSessionRequestBody, u as DiscoveredTable } from "./types-Dz1LQYvQ.mjs";
2
2
  import { createChatSessionHandler } from "./chat-session.mjs";
3
3
  import { ClivlyEntitiesConfig, ConfigValidationError, ConfigValidationResult, CustomObjectDeclaration, DiscoveredSchemaTable, customObjectDeclarations, defineClivlyConfig, validateEntitiesConfig } from "@clivly/core/entity-config";
4
+ //#region src/trigger-url.d.ts
5
+ declare function isPlaceholderUrl(value: string): boolean;
6
+ //#endregion
4
7
  //#region src/errors.d.ts
5
8
  declare class ClivlyError extends Error {
6
9
  /** HTTP status, or null when the request never completed (network error). */
@@ -31,4 +34,4 @@ declare class ClivlyNetworkError extends ClivlyError {}
31
34
  */
32
35
  declare function createClivlySDK(config: ClivlySDKConfig): ClivlySDK;
33
36
  //#endregion
34
- 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 };
37
+ 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/dist/index.mjs CHANGED
@@ -124,11 +124,16 @@ function checkConceptCoverage(config) {
124
124
  }
125
125
  //#endregion
126
126
  //#region src/custom-slug.ts
127
+ const SLUG_PATTERN = /^[a-z][a-z0-9_-]*$/;
127
128
  const NON_ALNUM = /[^a-z0-9]+/g;
128
129
  const EDGE_HYPHENS = /(^-|-$)/g;
129
130
  function slugify(value) {
130
131
  return value.toLowerCase().trim().replace(NON_ALNUM, "-").replace(EDGE_HYPHENS, "");
131
132
  }
133
+ function suggestionFor(declared) {
134
+ const candidate = slugify(declared);
135
+ return candidate !== declared && SLUG_PATTERN.test(candidate) ? ` Did you mean "${candidate}"?` : "";
136
+ }
132
137
  function checkCustomObjectSlugs(config) {
133
138
  const custom = config.source?.custom ?? [];
134
139
  const declared = config.entities ? customObjectDeclarations$1(config.entities) : [];
@@ -142,12 +147,11 @@ function checkCustomObjectSlugs(config) {
142
147
  const seen = /* @__PURE__ */ new Map();
143
148
  for (const source of custom) {
144
149
  const declared = source.objectType;
145
- const canonical = slugify(declared);
146
- if (canonical === "") problems.push(`"${source.entity}" has an empty objectType.`);
147
- else if (canonical !== declared) problems.push(`"${declared}" is not a slug — the dashboard would store it as "${canonical}".`);
148
- const prior = seen.get(canonical);
149
- if (prior && canonical !== "") problems.push(`duplicate slug "${canonical}" declared by "${prior}" and "${source.entity}".`);
150
- else seen.set(canonical, source.entity);
150
+ if (declared === "") problems.push(`"${source.entity}" has an empty objectType.`);
151
+ 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)}`);
152
+ const prior = seen.get(declared);
153
+ if (prior && declared !== "") problems.push(`duplicate slug "${declared}" declared by "${prior}" and "${source.entity}".`);
154
+ else seen.set(declared, source.entity);
151
155
  }
152
156
  if (problems.length > 0) return {
153
157
  name: "Custom object slugs",
@@ -254,10 +258,20 @@ async function runMappingDryRun(config, entityKey) {
254
258
  detail: `"${resolved.source.entity}" is empty — nothing to map yet.`
255
259
  };
256
260
  const missing = Object.entries(fields).filter(([, column]) => !(column in row)).map(([field, column]) => `${field} → ${column}`);
261
+ if (missing.length === 0) return {
262
+ name,
263
+ ok: true,
264
+ detail: `Mapped ${Object.keys(fields).length} field(s) from a real "${resolved.source.entity}" row.`
265
+ };
266
+ if (config.entities?.entities?.[entityKey]?.projection !== void 0 && missing.length === Object.keys(fields).length) return {
267
+ name,
268
+ ok: false,
269
+ 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.`
270
+ };
257
271
  return {
258
272
  name,
259
- ok: missing.length === 0,
260
- 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(", ")}.`
273
+ ok: false,
274
+ detail: `Row from "${resolved.source.entity}" is missing mapped column(s): ${missing.join(", ")}.`
261
275
  };
262
276
  }
263
277
  //#endregion
@@ -466,9 +480,27 @@ async function mappingDryRunChecks(config) {
466
480
  return checks;
467
481
  }
468
482
  /**
483
+ * Why a source has no `DRIZZLE_META`, without asserting more than we know.
484
+ *
485
+ * There are two ways to get here and they are indistinguishable at runtime: the
486
+ * source really was hand-written, or it came from `fromDrizzle` and a spread
487
+ * stripped the metadata, which is non-enumerable. The second is easy to fall
488
+ * into precisely where `objectType` is present — `source.custom` is typed
489
+ * `Array<SyncSource & { objectType: string }>`, so attaching that property with
490
+ * `{ ...fromDrizzle(...), objectType }` used to be the only way to satisfy it.
491
+ *
492
+ * Flatly calling such a source "hand-written" sent developers looking for one
493
+ * that does not exist. Where `objectType` is present, name the likelier cause
494
+ * and the fix instead of guessing wrong with confidence.
495
+ */
496
+ function missingMetaDetail(source) {
497
+ const skipped = "column check skipped";
498
+ if (source.objectType === void 0) return `"${source.entity}" is a hand-written source — ${skipped}.`;
499
+ 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.`;
500
+ }
501
+ /**
469
502
  * Confirm each drizzle source actually has the cursor and id columns the sync
470
- * pages on. Hand-written sources have no column metadata, so they are skipped
471
- * rather than failed.
503
+ * pages on. Sources with no column metadata are skipped rather than failed.
472
504
  */
473
505
  function cursorColumnChecks(sources) {
474
506
  const checks = [];
@@ -479,7 +511,7 @@ function cursorColumnChecks(sources) {
479
511
  name: `Cursor/id columns exist (${source.entity})`,
480
512
  ok: false,
481
513
  skipped: true,
482
- detail: `"${source.entity}" is a hand-written source — column check skipped.`
514
+ detail: missingMetaDetail(source)
483
515
  });
484
516
  continue;
485
517
  }
@@ -1118,11 +1150,16 @@ function observedTriggerUrl(req) {
1118
1150
  return new URL(`${proto}://${host}${requestUrl.pathname}`).toString();
1119
1151
  } catch {}
1120
1152
  }
1153
+ const PLACEHOLDER_BRACKETS = /[<>]/;
1154
+ function isPlaceholderUrl(value) {
1155
+ return PLACEHOLDER_BRACKETS.test(value);
1156
+ }
1121
1157
  /**
1122
1158
  * Decide which trigger URL to report, in precedence order:
1123
1159
  *
1124
1160
  * 1. `configuredUrl` — an explicit `syncTrigger.url`. Someone typed this on
1125
- * purpose, so it beats anything inferred.
1161
+ * purpose, so it beats anything inferred. An unedited placeholder is not
1162
+ * "on purpose" and is skipped.
1126
1163
  * 2. platform env origin + `configuredPath` — available at boot, before any
1127
1164
  * request has arrived. Needs the path because platform variables only give
1128
1165
  * an origin.
@@ -1132,7 +1169,7 @@ function observedTriggerUrl(req) {
1132
1169
  * doesn't need to know about it.
1133
1170
  */
1134
1171
  function resolveTriggerUrl(args) {
1135
- if (args.configuredUrl) return args.configuredUrl;
1172
+ if (args.configuredUrl && !isPlaceholderUrl(args.configuredUrl)) return args.configuredUrl;
1136
1173
  const origin = originFromPlatformEnv(args.env);
1137
1174
  if (origin && args.configuredPath) return `${origin}${args.configuredPath.startsWith("/") ? args.configuredPath : `/${args.configuredPath}`}`;
1138
1175
  return args.observedUrl;
@@ -1873,4 +1910,4 @@ function createClivlySDK(config) {
1873
1910
  };
1874
1911
  }
1875
1912
  //#endregion
1876
- export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, validateEntitiesConfig };
1913
+ export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createChatSessionHandler, createClivlySDK, customObjectDeclarations, defineClivlyConfig, isPlaceholderUrl, validateEntitiesConfig };
@@ -331,6 +331,14 @@ interface SyncSource {
331
331
  }): Promise<Record<string, unknown>[]>;
332
332
  /** Stable, unique tie-breaker column for keyset paging. Defaults to "id". */
333
333
  idField?: string;
334
+ /**
335
+ * Custom-object slug this source mirrors into. Only meaningful for entries in
336
+ * `source.custom`, whose element type narrows it to required — declared here
337
+ * because the sync and readiness paths read `source.objectType` off a plain
338
+ * `SyncSource`, so leaving it off left the property required by one type,
339
+ * absent from another, and read by the runtime.
340
+ */
341
+ objectType?: string;
334
342
  /** Set by `fromDrizzle`; lets `doctor` introspect the backing table. */
335
343
  [DRIZZLE_META]?: DrizzleSourceMeta;
336
344
  }
@@ -331,6 +331,14 @@ interface SyncSource {
331
331
  }): Promise<Record<string, unknown>[]>;
332
332
  /** Stable, unique tie-breaker column for keyset paging. Defaults to "id". */
333
333
  idField?: string;
334
+ /**
335
+ * Custom-object slug this source mirrors into. Only meaningful for entries in
336
+ * `source.custom`, whose element type narrows it to required — declared here
337
+ * because the sync and readiness paths read `source.objectType` off a plain
338
+ * `SyncSource`, so leaving it off left the property required by one type,
339
+ * absent from another, and read by the runtime.
340
+ */
341
+ objectType?: string;
334
342
  /** Set by `fromDrizzle`; lets `doctor` introspect the backing table. */
335
343
  [DRIZZLE_META]?: DrizzleSourceMeta;
336
344
  }
package/dist/vite.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ClivlySDK } from "./types-Cgwxd4h_.cjs";
1
+ import { a as ClivlySDK } from "./types-Dz1LQYvQ.cjs";
2
2
  import { Plugin } from "vite";
3
3
  //#region src/vite.d.ts
4
4
  interface ClivlyViteOptions {
package/dist/vite.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ClivlySDK } from "./types-Cgwxd4h_.mjs";
1
+ import { a as ClivlySDK } from "./types-Dz1LQYvQ.mjs";
2
2
  import { Plugin } from "vite";
3
3
  //#region src/vite.d.ts
4
4
  interface ClivlyViteOptions {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clivly/sdk",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Clivly SDK — connect your app to Clivly Cloud (heartbeat, discovery, auth bridge).",
@@ -55,7 +55,7 @@
55
55
  "access": "public"
56
56
  },
57
57
  "dependencies": {
58
- "@clivly/core": "0.9.3"
58
+ "@clivly/core": "0.9.5"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "drizzle-orm": ">=0.30 || >=1.0.0-rc.0",