clivly 0.7.0 → 0.7.2
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 +103 -160
- package/core-entity-config.cjs +1 -2
- package/core-entity-config.mjs +1 -1
- package/core-entity-heuristics.cjs +1 -2
- package/core-entity-heuristics.mjs +1 -1
- package/core-mapping-score.cjs +1 -2
- package/core-mapping-score.mjs +1 -1
- package/core.cjs +1 -1
- package/core.mjs +1 -1
- package/index.cjs +18 -10
- package/index.mjs +18 -10
- package/package.json +3 -3
- package/{sdk-C_1sGiH8.mjs → sdk-C6WANW8M.mjs} +158 -23
- package/{sdk-CzOf596t.cjs → sdk-iqOXeWIp.cjs} +158 -23
- package/sdk.cjs +1 -1
- package/sdk.mjs +1 -1
package/README.md
CHANGED
|
@@ -1,34 +1,40 @@
|
|
|
1
1
|
# clivly
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
your app.
|
|
3
|
+
Clivly's consolidated public package.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
`runScheduled()` / `createClivlyHandler()`, see the
|
|
8
|
-
[connecting to Clivly Cloud guide](https://github.com/amani-joseph/clivly.com/blob/master/docs/guides/connecting-to-clivly-cloud.md).
|
|
5
|
+
It serves two roles:
|
|
9
6
|
|
|
10
|
-
|
|
7
|
+
- the CLI you run as `npx clivly ...`
|
|
8
|
+
- the package that exposes the stable runtime subpaths such as `clivly/sdk`
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
npm install clivly
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Stable releases publish on npm's `latest` tag. Use `@next` only when you are
|
|
17
|
+
intentionally testing a prerelease build.
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
This package requires Node `>=22`.
|
|
20
|
+
|
|
21
|
+
## CLI commands
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx clivly init # scaffold config, route files, and .env placeholders
|
|
25
|
+
npx clivly add widget # scaffold the chat-session route for the widget
|
|
26
|
+
npx clivly login # pair this machine and write credentials to .env
|
|
17
27
|
npx clivly status # check your setup, step by step
|
|
18
28
|
npx clivly dev # tunnel localhost so remote sync can reach you
|
|
19
29
|
npx clivly push-schema # report your schema to Clivly without deploying
|
|
20
30
|
```
|
|
21
31
|
|
|
22
|
-
`ping` and `doctor` remain as aliases
|
|
32
|
+
`ping` and `doctor` remain available as focused aliases around parts of the
|
|
33
|
+
`status` ladder.
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
`npx clivly …` commands above use your local copy. Use `@next` only when you
|
|
26
|
-
are intentionally testing a prerelease cut.
|
|
35
|
+
## Runtime imports
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
`clivly` is the consolidated public package. The CLI remains available as
|
|
31
|
-
`npx clivly`, while application code imports from typed subpaths:
|
|
37
|
+
Application code should import from subpaths, not from the package root:
|
|
32
38
|
|
|
33
39
|
```ts
|
|
34
40
|
import { createClivlySDK, defineClivlyConfig } from "clivly/sdk";
|
|
@@ -39,71 +45,43 @@ import { createWorkOSAuthAdapter } from "clivly/auth/workos";
|
|
|
39
45
|
import { createBetterAuthAdapter } from "clivly/auth/better-auth";
|
|
40
46
|
```
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
Do **not** import application code from `clivly` (the package root). The root
|
|
49
|
+
entry is the CLI.
|
|
50
|
+
|
|
51
|
+
Provider and framework integrations use optional peer dependencies. Install only
|
|
44
52
|
what your app actually uses, such as `drizzle-orm`, `vite`, `@clerk/backend`,
|
|
45
53
|
`@workos-inc/node`, or `better-auth`.
|
|
46
54
|
|
|
47
|
-
`clivly init`
|
|
48
|
-
schema, and generates a `clivly.config.ts` that maps likely contact and company
|
|
49
|
-
tables. The init command currently generates config for Drizzle only; Prisma and
|
|
50
|
-
Kysely projects should use a hand-written `fetchPage` source for now.
|
|
51
|
-
|
|
52
|
-
Use `--schema` when your Drizzle schema is not in a common location or in
|
|
53
|
-
`drizzle.config.ts`. Re-run with `--force` to overwrite an existing config.
|
|
54
|
-
|
|
55
|
-
### Monorepos
|
|
55
|
+
## `clivly init`
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
and
|
|
59
|
-
by a relative path that climbs out of your app:
|
|
60
|
-
|
|
61
|
-
```ts
|
|
62
|
-
import * as schema from "@acme/database"; // not "../../packages/database/src/index"
|
|
63
|
-
```
|
|
57
|
+
`clivly init` detects your framework from `package.json`, locates your Drizzle
|
|
58
|
+
schema, and writes:
|
|
64
59
|
|
|
65
|
-
|
|
60
|
+
- `clivly.config.ts`
|
|
61
|
+
- the supported Clivly route files for your framework
|
|
62
|
+
- missing `.env` placeholders
|
|
66
63
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
clivly init --schema ./src/db/schema.ts # a plain path (unchanged)
|
|
71
|
-
```
|
|
64
|
+
It currently targets Drizzle-backed integrations. Prisma and Kysely projects
|
|
65
|
+
can still integrate, but should provide hand-written `fetchPage` sources rather
|
|
66
|
+
than relying on the generated Drizzle path.
|
|
72
67
|
|
|
73
|
-
|
|
74
|
-
keep their meaning.
|
|
68
|
+
Useful flags:
|
|
75
69
|
|
|
76
70
|
| Flag | Purpose |
|
|
77
71
|
| --- | --- |
|
|
78
|
-
| `--schema <path\|package>` | Where your Drizzle
|
|
79
|
-
| `--db-import <specifier>` | Where the Drizzle
|
|
80
|
-
| `--auth-import <specifier>` | Where your
|
|
81
|
-
| `--contact-table <name>` |
|
|
82
|
-
| `--company-table <name>` |
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
you.
|
|
87
|
-
|
|
88
|
-
### When no contact entity can be found
|
|
72
|
+
| `--schema <path\|package>` | Where your Drizzle schema lives |
|
|
73
|
+
| `--db-import <specifier>` | Where the Drizzle client lives when it is not beside the schema |
|
|
74
|
+
| `--auth-import <specifier>` | Where your auth instance lives |
|
|
75
|
+
| `--contact-table <name>` | Force the contact entity table |
|
|
76
|
+
| `--company-table <name>` | Force the company entity table |
|
|
77
|
+
| `--force` | Overwrite generated files / `.env` values |
|
|
78
|
+
| `--no-login` | Skip the browser pairing step |
|
|
79
|
+
| `--no-browser` | Print the approval URL instead of opening it |
|
|
89
80
|
|
|
90
|
-
`init`
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
```
|
|
94
|
-
No usable contact entity
|
|
81
|
+
If no usable contact entity can be found, `init` writes nothing rather than
|
|
82
|
+
generating a broken config.
|
|
95
83
|
|
|
96
|
-
|
|
97
|
-
widgets
|
|
98
|
-
invoices
|
|
99
|
-
|
|
100
|
-
Pick the one that holds your people and re-run:
|
|
101
|
-
clivly init --schema @acme/database --contact-table widgets
|
|
102
|
-
|
|
103
|
-
Nothing was written — Clivly needs one entity before a config can load.
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
First-class route scaffolding currently exists for:
|
|
84
|
+
Supported first-class route scaffolding today:
|
|
107
85
|
|
|
108
86
|
- Next.js
|
|
109
87
|
- TanStack Start
|
|
@@ -111,82 +89,50 @@ First-class route scaffolding currently exists for:
|
|
|
111
89
|
- Remix
|
|
112
90
|
- Nuxt
|
|
113
91
|
|
|
114
|
-
|
|
115
|
-
layer for the tick and verify routes unless they are using Nuxt.
|
|
116
|
-
|
|
117
|
-
When `init` finishes and `CLIVLY_API_KEY` is still empty, it hands you straight
|
|
118
|
-
into `clivly login` so you don't end up staring at a blank `.env`. That step is
|
|
119
|
-
interactive, so it is skipped automatically when there is no TTY or when `CI` is
|
|
120
|
-
set — and you can always turn it off yourself:
|
|
92
|
+
## `clivly add widget`
|
|
121
93
|
|
|
122
94
|
```bash
|
|
123
|
-
npx clivly
|
|
124
|
-
npx clivly init --no-browser # pair, but print the URL instead of opening it
|
|
95
|
+
npx clivly add widget
|
|
125
96
|
```
|
|
126
97
|
|
|
98
|
+
Scaffolds the host-side widget session route that mounts
|
|
99
|
+
`createChatSessionHandler()` from `clivly/sdk`. It is idempotent and supports
|
|
100
|
+
`--dry-run` and `--force`.
|
|
101
|
+
|
|
127
102
|
## `clivly login`
|
|
128
103
|
|
|
129
104
|
```bash
|
|
130
105
|
npx clivly login [--force] [--rotate] [--api-url <url>] [--no-browser]
|
|
131
106
|
```
|
|
132
107
|
|
|
133
|
-
Pairs this machine with a Clivly workspace and writes
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
The pairing endpoints are feature-flagged server-side
|
|
153
|
-
(`CLIVLY_CLI_PAIRING_ENABLED`); if the backend you point at has not enabled
|
|
154
|
-
them, `clivly login` says so and exits non-zero.
|
|
155
|
-
|
|
156
|
-
**Clivly Cloud has pairing enabled**, so `clivly login` is the normal path there.
|
|
157
|
-
The flag defaults to `false`, so a self-hosted backend must opt in.
|
|
158
|
-
|
|
159
|
-
### Manual credential path
|
|
160
|
-
|
|
161
|
-
If pairing is unavailable — a self-hosted backend with the flag off, or a
|
|
162
|
-
restricted environment — every credential can be set by hand. Nothing about the
|
|
163
|
-
CLI requires pairing.
|
|
164
|
-
|
|
165
|
-
| Value | Where to get it | Notes |
|
|
166
|
-
| --- | --- | --- |
|
|
167
|
-
| `CLIVLY_API_KEY` | Dashboard → **Settings → API keys** → Create | **Shown once.** Copy it immediately; you cannot retrieve it later, only regenerate. Regenerating **revokes the previous key**, so redeploy after rotating. |
|
|
168
|
-
| `CLIVLY_SYNC_TRIGGER_SECRET` | Dashboard → **Integrations → Remote sync** | Must match the value in your app's env exactly. A mismatch surfaces as a `401` on the signed trigger. |
|
|
169
|
-
| `CLIVLY_SYNC_TRIGGER_URL` | Your own deployment | The public URL of your mounted tick route, e.g. `https://app.example.com/api/clivly/tick`. Optional until you deploy — push-only setups can leave it unset. |
|
|
170
|
-
| `CLIVLY_VERIFY_TOKEN` | Dashboard → **Integrations** | Only needed if you mount the auth-verify route. |
|
|
171
|
-
|
|
172
|
-
Write them into the `.env` of the package that imports `clivly`, then re-run
|
|
173
|
-
`clivly status` to confirm each rung goes green.
|
|
174
|
-
|
|
175
|
-
> **Running your own Clivly backend?** `CLIVLY_TRIGGER_SECRET_WRAPPING_KEY` (a
|
|
176
|
-
> 32-byte hex key) is a **hard production prerequisite** for pairing. The
|
|
177
|
-
> trigger secret is stored encrypted with it, so without the key set on the
|
|
178
|
-
> server, `clivly login` fails with a 500 at the moment it would hand over
|
|
179
|
-
> credentials. Set it before enabling `CLIVLY_CLI_PAIRING_ENABLED`.
|
|
108
|
+
Pairs this machine with a Clivly workspace and writes:
|
|
109
|
+
|
|
110
|
+
- `CLIVLY_SECRET_KEY`
|
|
111
|
+
- `CLIVLY_SYNC_TRIGGER_SECRET`
|
|
112
|
+
|
|
113
|
+
into your project's `.env`.
|
|
114
|
+
|
|
115
|
+
The approval URL is always printed, so headless or SSH sessions can still be
|
|
116
|
+
approved in another browser.
|
|
117
|
+
|
|
118
|
+
Notes:
|
|
119
|
+
|
|
120
|
+
- `--force` overwrites existing real values
|
|
121
|
+
- `--rotate` rotates the remote-sync trigger secret instead of revealing the
|
|
122
|
+
current one
|
|
123
|
+
- pairing is feature-flagged server-side with `CLIVLY_CLI_PAIRING_ENABLED`
|
|
124
|
+
|
|
125
|
+
If pairing is unavailable, you can still set credentials manually and then run
|
|
126
|
+
`clivly status`.
|
|
180
127
|
|
|
181
128
|
## `clivly status`
|
|
182
129
|
|
|
183
130
|
```bash
|
|
184
|
-
npx clivly status [--verbose] [--config <path>]
|
|
131
|
+
npx clivly status [--verbose] [--config <path>] [--env-file <path>]
|
|
185
132
|
```
|
|
186
133
|
|
|
187
|
-
Walks
|
|
188
|
-
connectivity
|
|
189
|
-
to do about it. This is the command to reach for when something isn't working.
|
|
134
|
+
Walks the setup one rung at a time: credentials, config, schema, sources, and
|
|
135
|
+
connectivity. This is the main diagnostic command for integration issues.
|
|
190
136
|
|
|
191
137
|
## `clivly dev`
|
|
192
138
|
|
|
@@ -195,52 +141,49 @@ npx clivly dev [--port <n>] [--api-url <url>] [--route-path <path>] [-- <cmd>]
|
|
|
195
141
|
```
|
|
196
142
|
|
|
197
143
|
Opens a `cloudflared` tunnel to your local app and registers it with Clivly for
|
|
198
|
-
the
|
|
199
|
-
|
|
200
|
-
explicitly after `--`. Needs `cloudflared` on `PATH` and `CLIVLY_API_KEY` set.
|
|
201
|
-
The override is cleared when the process exits; remote sync is otherwise
|
|
202
|
-
deploy-only.
|
|
144
|
+
the lifetime of the session, so remote sync can reach `localhost` during local
|
|
145
|
+
development. The override is cleared on exit.
|
|
203
146
|
|
|
204
|
-
|
|
147
|
+
Requires:
|
|
205
148
|
|
|
206
|
-
|
|
149
|
+
- `cloudflared` on `PATH`
|
|
150
|
+
- `CLIVLY_SECRET_KEY` set
|
|
151
|
+
|
|
152
|
+
## `clivly push-schema`
|
|
207
153
|
|
|
208
154
|
```bash
|
|
209
|
-
|
|
210
|
-
#
|
|
211
|
-
# Clivly ping
|
|
212
|
-
# ✅ Connected to Clivly
|
|
213
|
-
# Org: Acme (org_1)
|
|
155
|
+
npx clivly push-schema [--config <path>]
|
|
214
156
|
```
|
|
215
157
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
columns, sample fetch, and heartbeat.
|
|
158
|
+
Reports your discovered schema to Clivly without waiting for a deployed app to
|
|
159
|
+
heartbeat it first.
|
|
219
160
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
161
|
+
## `clivly ping` / `clivly doctor`
|
|
162
|
+
|
|
163
|
+
`clivly ping` sends one heartbeat and prints a terse pass/fail result.
|
|
164
|
+
|
|
165
|
+
`clivly doctor` loads your config and runs the same read-only checks exposed by
|
|
166
|
+
`await clivly.doctor()`.
|
|
224
167
|
|
|
225
168
|
## Telemetry
|
|
226
169
|
|
|
227
|
-
|
|
228
|
-
Clivly API you point it at, and only to do
|
|
229
|
-
schema pushes,
|
|
230
|
-
|
|
170
|
+
Clivly collects no telemetry. The CLI makes network requests only to the
|
|
171
|
+
Clivly API you point it at, and only to do the work you asked for: pairing,
|
|
172
|
+
heartbeats, schema pushes, status checks, and tunnel override registration.
|
|
173
|
+
|
|
174
|
+
See the telemetry section in:
|
|
175
|
+
[SECURITY.md](https://github.com/amani-joseph/clivly.com/blob/master/SECURITY.md#telemetry)
|
|
231
176
|
|
|
232
177
|
## More docs
|
|
233
178
|
|
|
234
|
-
-
|
|
235
|
-
[
|
|
236
|
-
-
|
|
237
|
-
[
|
|
238
|
-
- Security policy:
|
|
239
|
-
[SECURITY.md](https://github.com/amani-joseph/clivly.com/blob/master/SECURITY.md)
|
|
179
|
+
- Cloud onboarding guide:
|
|
180
|
+
[Connecting to Clivly Cloud](https://github.com/amani-joseph/clivly.com/blob/master/docs/guides/connecting-to-clivly-cloud.md)
|
|
181
|
+
- Chat widget guide:
|
|
182
|
+
[Adding the chat widget](https://github.com/amani-joseph/clivly.com/blob/master/docs/guides/adding-the-chat-widget.md)
|
|
240
183
|
- SDK docs:
|
|
241
|
-
[
|
|
184
|
+
[packages/sdk/README.md](https://github.com/amani-joseph/clivly.com/blob/master/packages/sdk/README.md)
|
|
242
185
|
- Release notes:
|
|
243
|
-
[CHANGELOG](https://github.com/amani-joseph/clivly.com/blob/master/CHANGELOG.md)
|
|
186
|
+
[CHANGELOG.md](https://github.com/amani-joseph/clivly.com/blob/master/CHANGELOG.md)
|
|
244
187
|
|
|
245
188
|
## License
|
|
246
189
|
|
package/core-entity-config.cjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_deprecate_subpath = require("./deprecate-subpath-B9ngvizt.cjs");
|
|
3
2
|
const require_entity_config = require("./entity-config-D87rV-tE.cjs");
|
|
4
3
|
//#region src/core-entity-config.ts
|
|
5
|
-
|
|
4
|
+
require("./deprecate-subpath-B9ngvizt.cjs").warnDeprecatedSubpath("clivly/core/entity-config", "clivly/internal/entity-config");
|
|
6
5
|
//#endregion
|
|
7
6
|
exports.CANONICAL_FIELDS = require_entity_config.CANONICAL_FIELDS;
|
|
8
7
|
exports.CRM_CONCEPTS = require_entity_config.CRM_CONCEPTS;
|
package/core-entity-config.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as warnDeprecatedSubpath } from "./deprecate-subpath-CmvZtCpE.mjs";
|
|
2
1
|
import { a as MATERIALIZING_CONCEPTS, c as filterSchema, d as validateEntitiesConfig, i as ClivlyConfigError, l as relationshipSchema, n as CRM_CONCEPTS, o as defineClivlyConfig, r as CUSTOM_CONCEPT, s as entitiesConfigSchema, t as CANONICAL_FIELDS, u as relationshipsSchema } from "./entity-config-va2bkH5o.mjs";
|
|
2
|
+
import { t as warnDeprecatedSubpath } from "./deprecate-subpath-CmvZtCpE.mjs";
|
|
3
3
|
//#region src/core-entity-config.ts
|
|
4
4
|
warnDeprecatedSubpath("clivly/core/entity-config", "clivly/internal/entity-config");
|
|
5
5
|
//#endregion
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_deprecate_subpath = require("./deprecate-subpath-B9ngvizt.cjs");
|
|
3
2
|
const require_internal_entity_heuristics = require("./internal-entity-heuristics.cjs");
|
|
4
3
|
//#region src/core-entity-heuristics.ts
|
|
5
|
-
|
|
4
|
+
require("./deprecate-subpath-B9ngvizt.cjs").warnDeprecatedSubpath("clivly/core/entity-heuristics", "clivly/internal/entity-heuristics");
|
|
6
5
|
//#endregion
|
|
7
6
|
exports.TARGET_CONCEPTS = require_internal_entity_heuristics.TARGET_CONCEPTS;
|
|
8
7
|
exports.matchesConcept = require_internal_entity_heuristics.matchesConcept;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as warnDeprecatedSubpath } from "./deprecate-subpath-CmvZtCpE.mjs";
|
|
2
1
|
import { TARGET_CONCEPTS, matchesConcept, requiredFieldsFor, scoreConcepts, scoreFieldMap, scoreRelationships, suggestCursorField, suggestDiscriminatorColumns, toFieldMap, topConcept, validateMapping } from "./internal-entity-heuristics.mjs";
|
|
2
|
+
import { t as warnDeprecatedSubpath } from "./deprecate-subpath-CmvZtCpE.mjs";
|
|
3
3
|
//#region src/core-entity-heuristics.ts
|
|
4
4
|
warnDeprecatedSubpath("clivly/core/entity-heuristics", "clivly/internal/entity-heuristics");
|
|
5
5
|
//#endregion
|
package/core-mapping-score.cjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_deprecate_subpath = require("./deprecate-subpath-B9ngvizt.cjs");
|
|
3
2
|
const require_mapping_score = require("./mapping-score-CaB3F4Uu.cjs");
|
|
4
3
|
//#region src/core-mapping-score.ts
|
|
5
|
-
|
|
4
|
+
require("./deprecate-subpath-B9ngvizt.cjs").warnDeprecatedSubpath("clivly/core/mapping-score", "clivly/internal/mapping-score");
|
|
6
5
|
//#endregion
|
|
7
6
|
exports.AMBIGUITY_MARGIN = require_mapping_score.AMBIGUITY_MARGIN;
|
|
8
7
|
exports.CONCEPT_TIERS = require_mapping_score.CONCEPT_TIERS;
|
package/core-mapping-score.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as warnDeprecatedSubpath } from "./deprecate-subpath-CmvZtCpE.mjs";
|
|
2
1
|
import { a as W, c as isTextLike, i as RELATIONSHIP_TIERS, l as isTimestampLike, n as CONCEPT_TIERS, o as isAmbiguous, r as FIELD_TIERS, s as isNumericLike, t as AMBIGUITY_MARGIN, u as tierFor } from "./mapping-score-C68uAqGB.mjs";
|
|
2
|
+
import { t as warnDeprecatedSubpath } from "./deprecate-subpath-CmvZtCpE.mjs";
|
|
3
3
|
//#region src/core-mapping-score.ts
|
|
4
4
|
warnDeprecatedSubpath("clivly/core/mapping-score", "clivly/internal/mapping-score");
|
|
5
5
|
//#endregion
|
package/core.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_deprecate_subpath = require("./deprecate-subpath-B9ngvizt.cjs");
|
|
3
2
|
const require_entity_config = require("./entity-config-D87rV-tE.cjs");
|
|
3
|
+
const require_deprecate_subpath = require("./deprecate-subpath-B9ngvizt.cjs");
|
|
4
4
|
const require_auth_adapter = require("./auth-adapter-CiEXcweZ.cjs");
|
|
5
5
|
const require_mapping_form = require("./mapping-form-BpfON_ME.cjs");
|
|
6
6
|
const require_mappings_config = require("./mappings-config-BsRADNRv.cjs");
|
package/core.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as warnDeprecatedSubpath } from "./deprecate-subpath-CmvZtCpE.mjs";
|
|
2
1
|
import { a as MATERIALIZING_CONCEPTS, c as filterSchema, d as validateEntitiesConfig, i as ClivlyConfigError, l as relationshipSchema, n as CRM_CONCEPTS, o as defineClivlyConfig, s as entitiesConfigSchema, t as CANONICAL_FIELDS, u as relationshipsSchema } from "./entity-config-va2bkH5o.mjs";
|
|
2
|
+
import { t as warnDeprecatedSubpath } from "./deprecate-subpath-CmvZtCpE.mjs";
|
|
3
3
|
import { t as authCustom } from "./auth-adapter-gfVDxf3d.mjs";
|
|
4
4
|
import { n as buildRelationships, r as relationshipViaSignature, t as buildFilterFromDiscriminator } from "./mapping-form-DigkuvFc.mjs";
|
|
5
5
|
import { t as mappingsToEntitiesConfig } from "./mappings-config-Dbgxx3Nv.mjs";
|
package/index.cjs
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const require_entity_config = require("./entity-config-D87rV-tE.cjs");
|
|
4
|
-
require("./core-entity-config.cjs");
|
|
5
4
|
require("./mapping-score-CaB3F4Uu.cjs");
|
|
6
5
|
const require_internal_entity_heuristics = require("./internal-entity-heuristics.cjs");
|
|
7
|
-
require("./
|
|
8
|
-
require("./core-mapping-score.cjs");
|
|
9
|
-
const require_sdk = require("./sdk-CzOf596t.cjs");
|
|
6
|
+
const require_sdk = require("./sdk-iqOXeWIp.cjs");
|
|
10
7
|
let node_child_process = require("node:child_process");
|
|
11
8
|
let node_fs = require("node:fs");
|
|
12
9
|
let node_module = require("node:module");
|
|
@@ -720,6 +717,12 @@ function siblingDbCandidates(schemaPath) {
|
|
|
720
717
|
const dir = isDirectory(schemaPath) ? schemaPath : (0, node_path.dirname)(schemaPath);
|
|
721
718
|
return SIBLING_DB_NAMES.map((name) => (0, node_path.join)(dir, name));
|
|
722
719
|
}
|
|
720
|
+
function parentDbCandidates(schemaPath, cwd) {
|
|
721
|
+
const parent = (0, node_path.dirname)(isDirectory(schemaPath) ? schemaPath : (0, node_path.dirname)(schemaPath));
|
|
722
|
+
const relativeToRoot = (0, node_path.relative)((0, node_path.resolve)(cwd), (0, node_path.resolve)(parent));
|
|
723
|
+
if (!(relativeToRoot === "" || !relativeToRoot.startsWith(".."))) return [];
|
|
724
|
+
return SIBLING_DB_NAMES.map((name) => (0, node_path.join)(parent, name));
|
|
725
|
+
}
|
|
723
726
|
async function discoverSchema(opts) {
|
|
724
727
|
const load = opts.load ?? defaultLoader$1(opts.cwd);
|
|
725
728
|
const mod = await loadSchemaModule(opts.schemaPath, load);
|
|
@@ -751,6 +754,13 @@ async function discoverSchema(opts) {
|
|
|
751
754
|
load,
|
|
752
755
|
match: isDrizzleDb
|
|
753
756
|
});
|
|
757
|
+
if (db === null) db = await probeModule({
|
|
758
|
+
candidates: parentDbCandidates(opts.schemaPath, opts.cwd),
|
|
759
|
+
cwd: opts.cwd,
|
|
760
|
+
graph: opts.graph,
|
|
761
|
+
load,
|
|
762
|
+
match: isDrizzleDb
|
|
763
|
+
});
|
|
754
764
|
return {
|
|
755
765
|
tables,
|
|
756
766
|
importPath,
|
|
@@ -1286,10 +1296,6 @@ function renderSource(role, entity, dbExpr) {
|
|
|
1286
1296
|
const opts = entity.idField === null ? `{ entity: "${entity.tableName}", cursorField: "${entity.cursorField}" }` : `{ entity: "${entity.tableName}", cursorField: "${entity.cursorField}", idField: "${entity.idField}" }`;
|
|
1287
1297
|
return ` ${role}: fromDrizzle(${dbExpr}, schema.${entity.moduleKey}, ${opts})`;
|
|
1288
1298
|
}
|
|
1289
|
-
function fallbackDbImportPath(schemaImportPath) {
|
|
1290
|
-
const slash = schemaImportPath.lastIndexOf("/");
|
|
1291
|
-
return `${slash >= 0 ? schemaImportPath.slice(0, slash) : "."}/db`;
|
|
1292
|
-
}
|
|
1293
1299
|
function dbIsInSchemaModule(plan) {
|
|
1294
1300
|
return plan.db !== null && plan.db.importPath === plan.importPath;
|
|
1295
1301
|
}
|
|
@@ -1304,7 +1310,7 @@ function renderImports(plan) {
|
|
|
1304
1310
|
"import { discoverFromDrizzle } from \"clivly/drizzle\";",
|
|
1305
1311
|
`import * as schema from "${plan.importPath}";`
|
|
1306
1312
|
];
|
|
1307
|
-
if (!plan.db) lines.push(
|
|
1313
|
+
if (!plan.db) lines.push("import { db } from \"<YOUR_DB_MODULE>\"; // TODO(clivly): replace with the path to your Drizzle `db` instance — NOT your schema file");
|
|
1308
1314
|
else if (!dbIsInSchemaModule(plan)) lines.push(`import { ${plan.db.moduleKey} } from "${plan.db.importPath}";`);
|
|
1309
1315
|
return lines.join("\n");
|
|
1310
1316
|
}
|
|
@@ -1333,6 +1339,8 @@ export default createClivlySDK({
|
|
|
1333
1339
|
// all when you add a mapping — not just the ones wired up as sources below.
|
|
1334
1340
|
schema: discoverFromDrizzle(schema),
|
|
1335
1341
|
source: {
|
|
1342
|
+
// Keyed by CRM concept — contacts/companies/deals/custom.
|
|
1343
|
+
// NOT by your entity key. The entity's own \`source:\` above is a table name.
|
|
1336
1344
|
${sources.join(",\n")},
|
|
1337
1345
|
},
|
|
1338
1346
|
syncTrigger: {
|
|
@@ -2133,7 +2141,7 @@ const STATUS_STAGES = [
|
|
|
2133
2141
|
return Promise.resolve({
|
|
2134
2142
|
state: "warn",
|
|
2135
2143
|
detail: "Not set — remote sync cannot call back into this app.",
|
|
2136
|
-
hint: "Set CLIVLY_SYNC_TRIGGER_URL once the app is deployed. Push-only setups can ignore this."
|
|
2144
|
+
hint: "Set CLIVLY_SYNC_TRIGGER_URL once the app is deployed. For local development run `clivly dev` — it opens a tunnel and registers the URL for you. Push-only setups can ignore this."
|
|
2137
2145
|
});
|
|
2138
2146
|
}
|
|
2139
2147
|
}
|
package/index.mjs
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as CANONICAL_FIELDS } from "./entity-config-va2bkH5o.mjs";
|
|
3
|
-
import "./core-entity-config.mjs";
|
|
4
3
|
import "./mapping-score-C68uAqGB.mjs";
|
|
5
4
|
import { matchesConcept, requiredFieldsFor, scoreFieldMap, scoreRelationships, suggestCursorField, toFieldMap } from "./internal-entity-heuristics.mjs";
|
|
6
|
-
import "./
|
|
7
|
-
import "./core-mapping-score.mjs";
|
|
8
|
-
import { t as createClivlySDK } from "./sdk-C_1sGiH8.mjs";
|
|
5
|
+
import { t as createClivlySDK } from "./sdk-C6WANW8M.mjs";
|
|
9
6
|
import { createRequire } from "node:module";
|
|
10
7
|
import { spawn } from "node:child_process";
|
|
11
8
|
import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -719,6 +716,12 @@ function siblingDbCandidates(schemaPath) {
|
|
|
719
716
|
const dir = isDirectory(schemaPath) ? schemaPath : dirname(schemaPath);
|
|
720
717
|
return SIBLING_DB_NAMES.map((name) => join(dir, name));
|
|
721
718
|
}
|
|
719
|
+
function parentDbCandidates(schemaPath, cwd) {
|
|
720
|
+
const parent = dirname(isDirectory(schemaPath) ? schemaPath : dirname(schemaPath));
|
|
721
|
+
const relativeToRoot = relative(resolve(cwd), resolve(parent));
|
|
722
|
+
if (!(relativeToRoot === "" || !relativeToRoot.startsWith(".."))) return [];
|
|
723
|
+
return SIBLING_DB_NAMES.map((name) => join(parent, name));
|
|
724
|
+
}
|
|
722
725
|
async function discoverSchema(opts) {
|
|
723
726
|
const load = opts.load ?? defaultLoader$1(opts.cwd);
|
|
724
727
|
const mod = await loadSchemaModule(opts.schemaPath, load);
|
|
@@ -750,6 +753,13 @@ async function discoverSchema(opts) {
|
|
|
750
753
|
load,
|
|
751
754
|
match: isDrizzleDb
|
|
752
755
|
});
|
|
756
|
+
if (db === null) db = await probeModule({
|
|
757
|
+
candidates: parentDbCandidates(opts.schemaPath, opts.cwd),
|
|
758
|
+
cwd: opts.cwd,
|
|
759
|
+
graph: opts.graph,
|
|
760
|
+
load,
|
|
761
|
+
match: isDrizzleDb
|
|
762
|
+
});
|
|
753
763
|
return {
|
|
754
764
|
tables,
|
|
755
765
|
importPath,
|
|
@@ -1285,10 +1295,6 @@ function renderSource(role, entity, dbExpr) {
|
|
|
1285
1295
|
const opts = entity.idField === null ? `{ entity: "${entity.tableName}", cursorField: "${entity.cursorField}" }` : `{ entity: "${entity.tableName}", cursorField: "${entity.cursorField}", idField: "${entity.idField}" }`;
|
|
1286
1296
|
return ` ${role}: fromDrizzle(${dbExpr}, schema.${entity.moduleKey}, ${opts})`;
|
|
1287
1297
|
}
|
|
1288
|
-
function fallbackDbImportPath(schemaImportPath) {
|
|
1289
|
-
const slash = schemaImportPath.lastIndexOf("/");
|
|
1290
|
-
return `${slash >= 0 ? schemaImportPath.slice(0, slash) : "."}/db`;
|
|
1291
|
-
}
|
|
1292
1298
|
function dbIsInSchemaModule(plan) {
|
|
1293
1299
|
return plan.db !== null && plan.db.importPath === plan.importPath;
|
|
1294
1300
|
}
|
|
@@ -1303,7 +1309,7 @@ function renderImports(plan) {
|
|
|
1303
1309
|
"import { discoverFromDrizzle } from \"clivly/drizzle\";",
|
|
1304
1310
|
`import * as schema from "${plan.importPath}";`
|
|
1305
1311
|
];
|
|
1306
|
-
if (!plan.db) lines.push(
|
|
1312
|
+
if (!plan.db) lines.push("import { db } from \"<YOUR_DB_MODULE>\"; // TODO(clivly): replace with the path to your Drizzle `db` instance — NOT your schema file");
|
|
1307
1313
|
else if (!dbIsInSchemaModule(plan)) lines.push(`import { ${plan.db.moduleKey} } from "${plan.db.importPath}";`);
|
|
1308
1314
|
return lines.join("\n");
|
|
1309
1315
|
}
|
|
@@ -1332,6 +1338,8 @@ export default createClivlySDK({
|
|
|
1332
1338
|
// all when you add a mapping — not just the ones wired up as sources below.
|
|
1333
1339
|
schema: discoverFromDrizzle(schema),
|
|
1334
1340
|
source: {
|
|
1341
|
+
// Keyed by CRM concept — contacts/companies/deals/custom.
|
|
1342
|
+
// NOT by your entity key. The entity's own \`source:\` above is a table name.
|
|
1335
1343
|
${sources.join(",\n")},
|
|
1336
1344
|
},
|
|
1337
1345
|
syncTrigger: {
|
|
@@ -2132,7 +2140,7 @@ const STATUS_STAGES = [
|
|
|
2132
2140
|
return Promise.resolve({
|
|
2133
2141
|
state: "warn",
|
|
2134
2142
|
detail: "Not set — remote sync cannot call back into this app.",
|
|
2135
|
-
hint: "Set CLIVLY_SYNC_TRIGGER_URL once the app is deployed. Push-only setups can ignore this."
|
|
2143
|
+
hint: "Set CLIVLY_SYNC_TRIGGER_URL once the app is deployed. For local development run `clivly dev` — it opens a tunnel and registers the URL for you. Push-only setups can ignore this."
|
|
2136
2144
|
});
|
|
2137
2145
|
}
|
|
2138
2146
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clivly",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Clivly CLI — scaffold a CRM into your app (clivly init).",
|
|
@@ -367,8 +367,8 @@
|
|
|
367
367
|
"peerDependencies": {
|
|
368
368
|
"@clerk/backend": ">=1",
|
|
369
369
|
"@workos-inc/node": ">=7",
|
|
370
|
-
"better-auth": ">=1",
|
|
371
|
-
"drizzle-orm": ">=0.30",
|
|
370
|
+
"better-auth": ">=1.6.22",
|
|
371
|
+
"drizzle-orm": ">=0.30 || >=1.0.0-rc.0",
|
|
372
372
|
"vite": ">=5"
|
|
373
373
|
},
|
|
374
374
|
"peerDependenciesMeta": {
|
|
@@ -1,4 +1,83 @@
|
|
|
1
1
|
import { t as DRIZZLE_META } from "./drizzle-meta-D-5k3n0i.mjs";
|
|
2
|
+
//#region ../sdk/src/entity-source.ts
|
|
3
|
+
const CONCEPT_KEYS = {
|
|
4
|
+
contact: "contacts",
|
|
5
|
+
company: "companies",
|
|
6
|
+
deal: "deals"
|
|
7
|
+
};
|
|
8
|
+
function resolveEntitySource(config, entityKey) {
|
|
9
|
+
const entity = config.entities?.entities?.[entityKey];
|
|
10
|
+
const src = config.source;
|
|
11
|
+
if (!(entity && src)) return null;
|
|
12
|
+
const conceptKey = CONCEPT_KEYS[entity.concept];
|
|
13
|
+
if (conceptKey) {
|
|
14
|
+
const source = src[conceptKey];
|
|
15
|
+
return source ? {
|
|
16
|
+
concept: entity.concept,
|
|
17
|
+
expectedKey: conceptKey,
|
|
18
|
+
source
|
|
19
|
+
} : null;
|
|
20
|
+
}
|
|
21
|
+
if (entity.concept === "custom") {
|
|
22
|
+
const match = (src.custom ?? []).find((c) => c.entity === entity.source || c.entity === entityKey);
|
|
23
|
+
return match ? {
|
|
24
|
+
concept: "custom",
|
|
25
|
+
expectedKey: "custom",
|
|
26
|
+
source: match
|
|
27
|
+
} : null;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/** The `config.source` key an entity's concept requires, present or not. */
|
|
32
|
+
function expectedSourceKey(concept) {
|
|
33
|
+
return CONCEPT_KEYS[concept] ?? "custom";
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region ../sdk/src/concept-coverage.ts
|
|
37
|
+
const CHECK_NAME = "Entity → source coverage";
|
|
38
|
+
function configuredKeys(config) {
|
|
39
|
+
const src = config.source;
|
|
40
|
+
if (!src) return [];
|
|
41
|
+
return Object.entries(src).filter(([, value]) => Array.isArray(value) ? value.length > 0 : Boolean(value)).map(([key]) => key);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Every configured entity must resolve to a source, or it silently syncs
|
|
45
|
+
* nothing. This is the check that catches `source` keyed by entity name rather
|
|
46
|
+
* than by CRM concept — a mistake that otherwise shows up only as a run that
|
|
47
|
+
* succeeds with zero rows.
|
|
48
|
+
*/
|
|
49
|
+
function checkConceptCoverage(config) {
|
|
50
|
+
const entities = config.entities?.entities ?? {};
|
|
51
|
+
const keys = Object.keys(entities);
|
|
52
|
+
if (keys.length === 0) return {
|
|
53
|
+
name: CHECK_NAME,
|
|
54
|
+
ok: false,
|
|
55
|
+
skipped: true,
|
|
56
|
+
detail: "No entities configured — nothing to cross-check."
|
|
57
|
+
};
|
|
58
|
+
const unresolved = keys.filter((key) => resolveEntitySource(config, key) === null);
|
|
59
|
+
if (unresolved.length === 0) return {
|
|
60
|
+
name: CHECK_NAME,
|
|
61
|
+
ok: true,
|
|
62
|
+
detail: `${keys.length} entit${keys.length === 1 ? "y" : "ies"} resolve to a configured source.`
|
|
63
|
+
};
|
|
64
|
+
const available = configuredKeys(config);
|
|
65
|
+
const listed = available.length > 0 ? available.join(", ") : "(none)";
|
|
66
|
+
const problems = unresolved.map((key) => {
|
|
67
|
+
const concept = entities[key]?.concept ?? "unknown";
|
|
68
|
+
if (concept === "custom") {
|
|
69
|
+
const custom = config.source?.custom ?? [];
|
|
70
|
+
if (custom.length > 0) return `"${key}" has concept "custom" but no entry in \`source.custom\` matches it — it will never sync. Entities configured in \`source.custom\`: ${custom.map((c) => c.entity).join(", ")}.`;
|
|
71
|
+
}
|
|
72
|
+
return `"${key}" has concept "${concept}" but no \`source.${expectedSourceKey(concept)}\` is configured — it will never sync.`;
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
name: CHECK_NAME,
|
|
76
|
+
ok: false,
|
|
77
|
+
detail: `${problems.join(" ")} Configured source keys: ${listed}. \`source\` is keyed by CRM concept, not by entity key.`
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
2
81
|
//#region ../sdk/src/custom-slug.ts
|
|
3
82
|
const NON_ALNUM = /[^a-z0-9]+/g;
|
|
4
83
|
const EDGE_HYPHENS = /(^-|-$)/g;
|
|
@@ -79,6 +158,56 @@ function describeFetchError(entity, error) {
|
|
|
79
158
|
return `Couldn't read "${entity}": ${firstLine(root instanceof Error ? root.message : String(root?.message ?? root ?? "unknown error"))}`;
|
|
80
159
|
}
|
|
81
160
|
//#endregion
|
|
161
|
+
//#region ../sdk/src/mapping-dry-run.ts
|
|
162
|
+
/**
|
|
163
|
+
* Fetch one real row and push it through the entity's field map.
|
|
164
|
+
*
|
|
165
|
+
* The distinction from "Sample fetch": that proves the query runs, this proves
|
|
166
|
+
* the row that comes back actually carries the columns `fields` names. A
|
|
167
|
+
* mapping that points at a column the row lacks produces empty CRM records
|
|
168
|
+
* rather than an error, so it has to be caught here.
|
|
169
|
+
*
|
|
170
|
+
* An empty table SKIPS rather than fails — a fresh database is not a
|
|
171
|
+
* misconfiguration, and failing on it would train people to ignore the check.
|
|
172
|
+
*/
|
|
173
|
+
async function runMappingDryRun(config, entityKey) {
|
|
174
|
+
const name = `Field mapping dry run (${entityKey})`;
|
|
175
|
+
const resolved = resolveEntitySource(config, entityKey);
|
|
176
|
+
if (!resolved) return {
|
|
177
|
+
name,
|
|
178
|
+
ok: false,
|
|
179
|
+
skipped: true,
|
|
180
|
+
detail: `"${entityKey}" resolves to no source — see the coverage check above.`
|
|
181
|
+
};
|
|
182
|
+
const fields = config.entities?.entities?.[entityKey]?.fields ?? {};
|
|
183
|
+
let rows;
|
|
184
|
+
try {
|
|
185
|
+
rows = await resolved.source.fetchPage({
|
|
186
|
+
since: null,
|
|
187
|
+
limit: 1
|
|
188
|
+
});
|
|
189
|
+
} catch (error) {
|
|
190
|
+
return {
|
|
191
|
+
name,
|
|
192
|
+
ok: false,
|
|
193
|
+
detail: describeFetchError(entityKey, error)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const row = rows[0];
|
|
197
|
+
if (row === void 0) return {
|
|
198
|
+
name,
|
|
199
|
+
ok: false,
|
|
200
|
+
skipped: true,
|
|
201
|
+
detail: `"${resolved.source.entity}" is empty — nothing to map yet.`
|
|
202
|
+
};
|
|
203
|
+
const missing = Object.entries(fields).filter(([, column]) => !(column in row)).map(([field, column]) => `${field} → ${column}`);
|
|
204
|
+
return {
|
|
205
|
+
name,
|
|
206
|
+
ok: missing.length === 0,
|
|
207
|
+
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(", ")}.`
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
82
211
|
//#region ../sdk/src/namespace-probe.ts
|
|
83
212
|
/**
|
|
84
213
|
* Verify the tables the integration depends on are genuinely reachable:
|
|
@@ -169,6 +298,11 @@ async function namespaceCheck(config) {
|
|
|
169
298
|
detail: probeResult.reachable ? "All required source tables are reachable." : probeResult.error?.message ?? "namespace unreachable"
|
|
170
299
|
};
|
|
171
300
|
}
|
|
301
|
+
async function mappingDryRunChecks(config) {
|
|
302
|
+
const checks = [];
|
|
303
|
+
for (const entityKey of Object.keys(config.entities?.entities ?? {})) checks.push(await runMappingDryRun(config, entityKey));
|
|
304
|
+
return checks;
|
|
305
|
+
}
|
|
172
306
|
/**
|
|
173
307
|
* Run the read-only setup checks and return a structured report. Every check
|
|
174
308
|
* captures its own error into `detail` rather than throwing, so one failure never
|
|
@@ -195,6 +329,7 @@ async function runDoctor(args) {
|
|
|
195
329
|
ok: sources.length > 0,
|
|
196
330
|
detail: sources.length > 0 ? `${sources.length} source(s) to mirror.` : "No `source.contacts` — nothing to sync."
|
|
197
331
|
});
|
|
332
|
+
checks.push(checkConceptCoverage(config));
|
|
198
333
|
for (const source of sources) {
|
|
199
334
|
const meta = source[DRIZZLE_META];
|
|
200
335
|
if (!meta) {
|
|
@@ -233,6 +368,7 @@ async function runDoctor(args) {
|
|
|
233
368
|
detail: describeFetchError(source.entity, error)
|
|
234
369
|
});
|
|
235
370
|
}
|
|
371
|
+
checks.push(...await mappingDryRunChecks(config));
|
|
236
372
|
const result = await connect();
|
|
237
373
|
checks.push({
|
|
238
374
|
name: "Heartbeat",
|
|
@@ -276,6 +412,7 @@ async function syncSource(args) {
|
|
|
276
412
|
const idField = source.idField ?? DEFAULT_ID_FIELD;
|
|
277
413
|
let cursor = since;
|
|
278
414
|
let pushed = 0;
|
|
415
|
+
const resumedFrom = since === null ? null : since.time.toISOString();
|
|
279
416
|
for (;;) {
|
|
280
417
|
const rows = await source.fetchPage({
|
|
281
418
|
since: cursor,
|
|
@@ -287,6 +424,8 @@ async function syncSource(args) {
|
|
|
287
424
|
source: source.entity,
|
|
288
425
|
rows: capped,
|
|
289
426
|
mode: "delta",
|
|
427
|
+
fetched: rows.length,
|
|
428
|
+
cursor: resumedFrom,
|
|
290
429
|
...objectType ? { objectType } : {}
|
|
291
430
|
})) return {
|
|
292
431
|
pushed,
|
|
@@ -329,6 +468,8 @@ async function fullSyncSource(args) {
|
|
|
329
468
|
source: source.entity,
|
|
330
469
|
rows: all,
|
|
331
470
|
mode: "full",
|
|
471
|
+
fetched: all.length,
|
|
472
|
+
cursor: null,
|
|
332
473
|
...objectType ? { objectType } : {}
|
|
333
474
|
});
|
|
334
475
|
return {
|
|
@@ -708,31 +849,25 @@ function createFetcher(retry) {
|
|
|
708
849
|
}
|
|
709
850
|
};
|
|
710
851
|
}
|
|
852
|
+
const CONCEPT_PATHS = {
|
|
853
|
+
contacts: SYNC_CONTACTS_PATH,
|
|
854
|
+
companies: SYNC_COMPANIES_PATH,
|
|
855
|
+
deals: SYNC_DEALS_PATH,
|
|
856
|
+
custom: SYNC_CUSTOM_PATH
|
|
857
|
+
};
|
|
711
858
|
function resolveSampleSource(config, entityKey) {
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
if (
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
if (entity.concept === "deal" && src.deals) return {
|
|
724
|
-
source: src.deals,
|
|
725
|
-
path: SYNC_DEALS_PATH
|
|
859
|
+
const resolved = resolveEntitySource(config, entityKey);
|
|
860
|
+
if (!resolved) return null;
|
|
861
|
+
const path = CONCEPT_PATHS[resolved.expectedKey];
|
|
862
|
+
if (!path) return null;
|
|
863
|
+
return resolved.expectedKey === "custom" ? {
|
|
864
|
+
source: resolved.source,
|
|
865
|
+
path,
|
|
866
|
+
objectType: resolved.source.objectType
|
|
867
|
+
} : {
|
|
868
|
+
source: resolved.source,
|
|
869
|
+
path
|
|
726
870
|
};
|
|
727
|
-
if (entity.concept === "custom") {
|
|
728
|
-
const custom = (src.custom ?? []).find((c) => c.entity === entity.source || c.entity === entityKey);
|
|
729
|
-
return custom ? {
|
|
730
|
-
source: custom,
|
|
731
|
-
path: SYNC_CUSTOM_PATH,
|
|
732
|
-
objectType: custom.objectType
|
|
733
|
-
} : null;
|
|
734
|
-
}
|
|
735
|
-
return null;
|
|
736
871
|
}
|
|
737
872
|
const PREVIEW_LIMIT = 5;
|
|
738
873
|
function resolvePreviewSource(config, sourceTable) {
|
|
@@ -1,4 +1,83 @@
|
|
|
1
1
|
const require_drizzle_meta = require("./drizzle-meta-Bb1JWYqf.cjs");
|
|
2
|
+
//#region ../sdk/src/entity-source.ts
|
|
3
|
+
const CONCEPT_KEYS = {
|
|
4
|
+
contact: "contacts",
|
|
5
|
+
company: "companies",
|
|
6
|
+
deal: "deals"
|
|
7
|
+
};
|
|
8
|
+
function resolveEntitySource(config, entityKey) {
|
|
9
|
+
const entity = config.entities?.entities?.[entityKey];
|
|
10
|
+
const src = config.source;
|
|
11
|
+
if (!(entity && src)) return null;
|
|
12
|
+
const conceptKey = CONCEPT_KEYS[entity.concept];
|
|
13
|
+
if (conceptKey) {
|
|
14
|
+
const source = src[conceptKey];
|
|
15
|
+
return source ? {
|
|
16
|
+
concept: entity.concept,
|
|
17
|
+
expectedKey: conceptKey,
|
|
18
|
+
source
|
|
19
|
+
} : null;
|
|
20
|
+
}
|
|
21
|
+
if (entity.concept === "custom") {
|
|
22
|
+
const match = (src.custom ?? []).find((c) => c.entity === entity.source || c.entity === entityKey);
|
|
23
|
+
return match ? {
|
|
24
|
+
concept: "custom",
|
|
25
|
+
expectedKey: "custom",
|
|
26
|
+
source: match
|
|
27
|
+
} : null;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/** The `config.source` key an entity's concept requires, present or not. */
|
|
32
|
+
function expectedSourceKey(concept) {
|
|
33
|
+
return CONCEPT_KEYS[concept] ?? "custom";
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region ../sdk/src/concept-coverage.ts
|
|
37
|
+
const CHECK_NAME = "Entity → source coverage";
|
|
38
|
+
function configuredKeys(config) {
|
|
39
|
+
const src = config.source;
|
|
40
|
+
if (!src) return [];
|
|
41
|
+
return Object.entries(src).filter(([, value]) => Array.isArray(value) ? value.length > 0 : Boolean(value)).map(([key]) => key);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Every configured entity must resolve to a source, or it silently syncs
|
|
45
|
+
* nothing. This is the check that catches `source` keyed by entity name rather
|
|
46
|
+
* than by CRM concept — a mistake that otherwise shows up only as a run that
|
|
47
|
+
* succeeds with zero rows.
|
|
48
|
+
*/
|
|
49
|
+
function checkConceptCoverage(config) {
|
|
50
|
+
const entities = config.entities?.entities ?? {};
|
|
51
|
+
const keys = Object.keys(entities);
|
|
52
|
+
if (keys.length === 0) return {
|
|
53
|
+
name: CHECK_NAME,
|
|
54
|
+
ok: false,
|
|
55
|
+
skipped: true,
|
|
56
|
+
detail: "No entities configured — nothing to cross-check."
|
|
57
|
+
};
|
|
58
|
+
const unresolved = keys.filter((key) => resolveEntitySource(config, key) === null);
|
|
59
|
+
if (unresolved.length === 0) return {
|
|
60
|
+
name: CHECK_NAME,
|
|
61
|
+
ok: true,
|
|
62
|
+
detail: `${keys.length} entit${keys.length === 1 ? "y" : "ies"} resolve to a configured source.`
|
|
63
|
+
};
|
|
64
|
+
const available = configuredKeys(config);
|
|
65
|
+
const listed = available.length > 0 ? available.join(", ") : "(none)";
|
|
66
|
+
const problems = unresolved.map((key) => {
|
|
67
|
+
const concept = entities[key]?.concept ?? "unknown";
|
|
68
|
+
if (concept === "custom") {
|
|
69
|
+
const custom = config.source?.custom ?? [];
|
|
70
|
+
if (custom.length > 0) return `"${key}" has concept "custom" but no entry in \`source.custom\` matches it — it will never sync. Entities configured in \`source.custom\`: ${custom.map((c) => c.entity).join(", ")}.`;
|
|
71
|
+
}
|
|
72
|
+
return `"${key}" has concept "${concept}" but no \`source.${expectedSourceKey(concept)}\` is configured — it will never sync.`;
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
name: CHECK_NAME,
|
|
76
|
+
ok: false,
|
|
77
|
+
detail: `${problems.join(" ")} Configured source keys: ${listed}. \`source\` is keyed by CRM concept, not by entity key.`
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
2
81
|
//#region ../sdk/src/custom-slug.ts
|
|
3
82
|
const NON_ALNUM = /[^a-z0-9]+/g;
|
|
4
83
|
const EDGE_HYPHENS = /(^-|-$)/g;
|
|
@@ -79,6 +158,56 @@ function describeFetchError(entity, error) {
|
|
|
79
158
|
return `Couldn't read "${entity}": ${firstLine(root instanceof Error ? root.message : String(root?.message ?? root ?? "unknown error"))}`;
|
|
80
159
|
}
|
|
81
160
|
//#endregion
|
|
161
|
+
//#region ../sdk/src/mapping-dry-run.ts
|
|
162
|
+
/**
|
|
163
|
+
* Fetch one real row and push it through the entity's field map.
|
|
164
|
+
*
|
|
165
|
+
* The distinction from "Sample fetch": that proves the query runs, this proves
|
|
166
|
+
* the row that comes back actually carries the columns `fields` names. A
|
|
167
|
+
* mapping that points at a column the row lacks produces empty CRM records
|
|
168
|
+
* rather than an error, so it has to be caught here.
|
|
169
|
+
*
|
|
170
|
+
* An empty table SKIPS rather than fails — a fresh database is not a
|
|
171
|
+
* misconfiguration, and failing on it would train people to ignore the check.
|
|
172
|
+
*/
|
|
173
|
+
async function runMappingDryRun(config, entityKey) {
|
|
174
|
+
const name = `Field mapping dry run (${entityKey})`;
|
|
175
|
+
const resolved = resolveEntitySource(config, entityKey);
|
|
176
|
+
if (!resolved) return {
|
|
177
|
+
name,
|
|
178
|
+
ok: false,
|
|
179
|
+
skipped: true,
|
|
180
|
+
detail: `"${entityKey}" resolves to no source — see the coverage check above.`
|
|
181
|
+
};
|
|
182
|
+
const fields = config.entities?.entities?.[entityKey]?.fields ?? {};
|
|
183
|
+
let rows;
|
|
184
|
+
try {
|
|
185
|
+
rows = await resolved.source.fetchPage({
|
|
186
|
+
since: null,
|
|
187
|
+
limit: 1
|
|
188
|
+
});
|
|
189
|
+
} catch (error) {
|
|
190
|
+
return {
|
|
191
|
+
name,
|
|
192
|
+
ok: false,
|
|
193
|
+
detail: describeFetchError(entityKey, error)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const row = rows[0];
|
|
197
|
+
if (row === void 0) return {
|
|
198
|
+
name,
|
|
199
|
+
ok: false,
|
|
200
|
+
skipped: true,
|
|
201
|
+
detail: `"${resolved.source.entity}" is empty — nothing to map yet.`
|
|
202
|
+
};
|
|
203
|
+
const missing = Object.entries(fields).filter(([, column]) => !(column in row)).map(([field, column]) => `${field} → ${column}`);
|
|
204
|
+
return {
|
|
205
|
+
name,
|
|
206
|
+
ok: missing.length === 0,
|
|
207
|
+
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(", ")}.`
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
82
211
|
//#region ../sdk/src/namespace-probe.ts
|
|
83
212
|
/**
|
|
84
213
|
* Verify the tables the integration depends on are genuinely reachable:
|
|
@@ -169,6 +298,11 @@ async function namespaceCheck(config) {
|
|
|
169
298
|
detail: probeResult.reachable ? "All required source tables are reachable." : probeResult.error?.message ?? "namespace unreachable"
|
|
170
299
|
};
|
|
171
300
|
}
|
|
301
|
+
async function mappingDryRunChecks(config) {
|
|
302
|
+
const checks = [];
|
|
303
|
+
for (const entityKey of Object.keys(config.entities?.entities ?? {})) checks.push(await runMappingDryRun(config, entityKey));
|
|
304
|
+
return checks;
|
|
305
|
+
}
|
|
172
306
|
/**
|
|
173
307
|
* Run the read-only setup checks and return a structured report. Every check
|
|
174
308
|
* captures its own error into `detail` rather than throwing, so one failure never
|
|
@@ -195,6 +329,7 @@ async function runDoctor(args) {
|
|
|
195
329
|
ok: sources.length > 0,
|
|
196
330
|
detail: sources.length > 0 ? `${sources.length} source(s) to mirror.` : "No `source.contacts` — nothing to sync."
|
|
197
331
|
});
|
|
332
|
+
checks.push(checkConceptCoverage(config));
|
|
198
333
|
for (const source of sources) {
|
|
199
334
|
const meta = source[require_drizzle_meta.DRIZZLE_META];
|
|
200
335
|
if (!meta) {
|
|
@@ -233,6 +368,7 @@ async function runDoctor(args) {
|
|
|
233
368
|
detail: describeFetchError(source.entity, error)
|
|
234
369
|
});
|
|
235
370
|
}
|
|
371
|
+
checks.push(...await mappingDryRunChecks(config));
|
|
236
372
|
const result = await connect();
|
|
237
373
|
checks.push({
|
|
238
374
|
name: "Heartbeat",
|
|
@@ -276,6 +412,7 @@ async function syncSource(args) {
|
|
|
276
412
|
const idField = source.idField ?? DEFAULT_ID_FIELD;
|
|
277
413
|
let cursor = since;
|
|
278
414
|
let pushed = 0;
|
|
415
|
+
const resumedFrom = since === null ? null : since.time.toISOString();
|
|
279
416
|
for (;;) {
|
|
280
417
|
const rows = await source.fetchPage({
|
|
281
418
|
since: cursor,
|
|
@@ -287,6 +424,8 @@ async function syncSource(args) {
|
|
|
287
424
|
source: source.entity,
|
|
288
425
|
rows: capped,
|
|
289
426
|
mode: "delta",
|
|
427
|
+
fetched: rows.length,
|
|
428
|
+
cursor: resumedFrom,
|
|
290
429
|
...objectType ? { objectType } : {}
|
|
291
430
|
})) return {
|
|
292
431
|
pushed,
|
|
@@ -329,6 +468,8 @@ async function fullSyncSource(args) {
|
|
|
329
468
|
source: source.entity,
|
|
330
469
|
rows: all,
|
|
331
470
|
mode: "full",
|
|
471
|
+
fetched: all.length,
|
|
472
|
+
cursor: null,
|
|
332
473
|
...objectType ? { objectType } : {}
|
|
333
474
|
});
|
|
334
475
|
return {
|
|
@@ -708,31 +849,25 @@ function createFetcher(retry) {
|
|
|
708
849
|
}
|
|
709
850
|
};
|
|
710
851
|
}
|
|
852
|
+
const CONCEPT_PATHS = {
|
|
853
|
+
contacts: SYNC_CONTACTS_PATH,
|
|
854
|
+
companies: SYNC_COMPANIES_PATH,
|
|
855
|
+
deals: SYNC_DEALS_PATH,
|
|
856
|
+
custom: SYNC_CUSTOM_PATH
|
|
857
|
+
};
|
|
711
858
|
function resolveSampleSource(config, entityKey) {
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
if (
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
if (entity.concept === "deal" && src.deals) return {
|
|
724
|
-
source: src.deals,
|
|
725
|
-
path: SYNC_DEALS_PATH
|
|
859
|
+
const resolved = resolveEntitySource(config, entityKey);
|
|
860
|
+
if (!resolved) return null;
|
|
861
|
+
const path = CONCEPT_PATHS[resolved.expectedKey];
|
|
862
|
+
if (!path) return null;
|
|
863
|
+
return resolved.expectedKey === "custom" ? {
|
|
864
|
+
source: resolved.source,
|
|
865
|
+
path,
|
|
866
|
+
objectType: resolved.source.objectType
|
|
867
|
+
} : {
|
|
868
|
+
source: resolved.source,
|
|
869
|
+
path
|
|
726
870
|
};
|
|
727
|
-
if (entity.concept === "custom") {
|
|
728
|
-
const custom = (src.custom ?? []).find((c) => c.entity === entity.source || c.entity === entityKey);
|
|
729
|
-
return custom ? {
|
|
730
|
-
source: custom,
|
|
731
|
-
path: SYNC_CUSTOM_PATH,
|
|
732
|
-
objectType: custom.objectType
|
|
733
|
-
} : null;
|
|
734
|
-
}
|
|
735
|
-
return null;
|
|
736
871
|
}
|
|
737
872
|
const PREVIEW_LIMIT = 5;
|
|
738
873
|
function resolvePreviewSource(config, sourceTable) {
|
package/sdk.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_entity_config = require("./entity-config-D87rV-tE.cjs");
|
|
3
|
-
const require_sdk = require("./sdk-
|
|
3
|
+
const require_sdk = require("./sdk-iqOXeWIp.cjs");
|
|
4
4
|
exports.ClivlyAuthError = require_sdk.ClivlyAuthError;
|
|
5
5
|
exports.ClivlyError = require_sdk.ClivlyError;
|
|
6
6
|
exports.ClivlyNetworkError = require_sdk.ClivlyNetworkError;
|
package/sdk.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { o as defineClivlyConfig } from "./entity-config-va2bkH5o.mjs";
|
|
2
|
-
import { a as ClivlyRateLimitError, i as ClivlyNetworkError, n as ClivlyAuthError, o as createChatSessionHandler, r as ClivlyError, t as createClivlySDK } from "./sdk-
|
|
2
|
+
import { a as ClivlyRateLimitError, i as ClivlyNetworkError, n as ClivlyAuthError, o as createChatSessionHandler, r as ClivlyError, t as createClivlySDK } from "./sdk-C6WANW8M.mjs";
|
|
3
3
|
export { ClivlyAuthError, ClivlyError, ClivlyNetworkError, ClivlyRateLimitError, createChatSessionHandler, createClivlySDK, defineClivlyConfig };
|