@datarails-ai/dr-cli 1.6.80 → 1.6.82

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/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import { fileURLToPath as fileURLToPath3 } from "url";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "@datarails-ai/dr-cli",
11
- version: "1.6.80",
11
+ version: "1.6.82",
12
12
  description: "Datarails CLI",
13
13
  type: "module",
14
14
  engines: {
@@ -38,7 +38,7 @@ var package_default = {
38
38
  postinstall: "node scripts/postinstall.mjs"
39
39
  },
40
40
  dependencies: {
41
- "@datarails-ai/dr-appspace-cli": "0.10.4",
41
+ "@datarails-ai/dr-appspace-cli": "0.10.5",
42
42
  "@modelcontextprotocol/sdk": "^1.29.0",
43
43
  "@napi-rs/keyring": "^1.3.0",
44
44
  commander: "^12.1.0",
@@ -67,7 +67,7 @@ var package_default = {
67
67
  };
68
68
 
69
69
  // src/buildInfo.ts
70
- var BUILD_TIME = "2026-09-30T14:50:43.133Z";
70
+ var BUILD_TIME = "2026-09-30T16:01:09.322Z";
71
71
  function formatBuildTime(iso) {
72
72
  if (iso === "") return "";
73
73
  const d = new Date(iso);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "cliVersion": "1.6.80",
3
+ "cliVersion": "1.6.82",
4
4
  "_contract": "Resolve a command path through `aliases` (canonical prefix + alias segment -> canonical path) before any lookup. A path listed in `pendingCommands`, or whose first segment is a key of `pending`, is UNDECIDED, not unrestricted: fail closed or skip it, and never read a path's absence from `commands` as 'no requirement'. An entry carrying `denied` is UNAVAILABLE to any AGENT-SCOPED credential — one whose `scopes` claim carries at least one `resource:action` entry; a claim that is absent, empty, capability-only or wholly unparseable is UNRESTRICTED, and this mark does not apply to it — whatever else that entry says: it is not a ceiling and it is not always-available, its `denied.reason` says why and `denied.until` names the ticket that lifts it, and the absence of `requires` on it must NEVER be read as 'no requirement'. Only an entry in `commands` with an empty `requires` means always-available.",
5
5
  "commands": {
6
6
  "login": {
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: create-connector-lib
3
+ description: Use when a vendor's connector code should be shared across Datarails orgs in dr-connectors - a vendor with no `lib/<vendor>/` yet that a second customer is likely to need, a Workato migration of a vendor we already run for another org, a second org asking for the same vendor with different columns, or an existing per-org connector being moved onto `lib/`. Triggers - "create a lib for <vendor>", "make <vendor> a shared lib", "generic connector", "pass the field names", "select_query", "the next customer needs different columns", "move this connector onto lib", "lib/<vendor>", "same vendor, another org".
4
+ ---
5
+
6
+ # Create a connector lib
7
+
8
+ ## Overview
9
+
10
+ A lib is the vendor's code written once: `lib/<vendor>/` owns the request, auth, pagination and the
11
+ file format, and each org's connector is two thin files that **pass their field names** plus a few
12
+ optional parameters. The worked example is `lib/ramp/dataqueries/select_query.py`; find its thin
13
+ connectors with `grep -rl "lib.ramp.dataqueries.select_query" --include=*.py us us2 uk ca`. Read both first.
14
+
15
+ **REQUIRED:** `datarails-connectors-onboard-connector` for everything around the code: branch,
16
+ publish, datasource, filebox, binding. Its memory, streaming and 4xx rules apply to lib code too.
17
+
18
+ ## Layout
19
+
20
+ ```
21
+ lib/<vendor>/
22
+ __init__.py what the lib holds: each module and its output, no one who uses it
23
+ _core.py output-neutral: credentials, token, client, pagination, query vars, logger
24
+ connections/base_connection.py the one connection: auth only, no scope or other per-customer value
25
+ dataqueries/select_query.py the generic engine
26
+ tests/test_select_query.py the engine, on synthetic data
27
+ tests/test_artifact.py every thin connector that imports lib.<vendor>, found by glob
28
+ <dc>/<org_id>_(<domain>)/<vendor>/
29
+ connections/<conn>.py subclasses the lib connection and sets its SCOPE (or other auth parameter)
30
+ dataqueries/<dq>.py the org's COLUMNS, OBJECT_TYPES, get_query_class
31
+ tests/test_<dq>.py the org's exact header, each org-specific source it uses, and its SCOPE
32
+ ```
33
+
34
+ ## The engine contract
35
+
36
+ The connector's thin dataquery subclasses `SelectQuery` and sets only data:
37
+
38
+ | Attribute | Meaning |
39
+ |---|---|
40
+ | `PATH` | endpoint under the vendor base URL |
41
+ | `WINDOW` | the endpoint's from/to parameter names, filled from the Sync-query's `startDate`/`endDate`; omit for undated datasets |
42
+ | `COLUMNS` | `(label, source)` pairs in file order |
43
+
44
+ Sources are small functions in the engine module: `field("a.b.c")` (dotted path; a missing step is
45
+ empty), `timestamp(...)` (re-rendered in the source system's format), and one per vendor-specific
46
+ lookup (Ramp: `category(remote_name, key)`; a custom-fields array would be `custom_field(name)`).
47
+ Add a source only when a column needs it. Add an engine attribute (e.g. `EXPLODE` for one row per line
48
+ item) only when a real connector needs it, with its test.
49
+
50
+ `OBJECT_TYPES` is a literal in the thin file. The engine module declares no columns, no object types,
51
+ no `get_query_class`. The connection works the same way: one lib connection whose parameters (Ramp:
52
+ `SCOPE`, the read scopes the customer's app was granted) the thin connection sets; an unset required
53
+ parameter fails in `connect()` before the vendor is called.
54
+
55
+ ## Rules
56
+
57
+ - **No company names anywhere under `lib/`**: no customer, org id, domain, or the tool being replaced
58
+ (Workato), in code, docstrings, comments, test names, fixture data or test ids. Test data is
59
+ synthetic. Per-customer values (columns, category or custom-field names) live only in thin files.
60
+ Before committing, grep the lib diff for the customer's name, domain and org id.
61
+ - **Reuse before adding.** Use `_core` helpers and an existing connection that fits; never a second
62
+ copy. Name modules after what they do, never after who uses them. Hand the user a list of lib
63
+ improvements you noticed (duplicated helpers, code that belongs in `_core`), done in their own PR.
64
+ - **Never change what another org lands.** A change to a module other orgs import must be proven
65
+ output-neutral: their tests pass, and for text-only edits, each module's AST with docstrings stripped
66
+ equals `origin/main`'s. A lib merge cuts new versions of every connector that bundles it; none moves
67
+ until its binding is repointed.
68
+ - **Take the file format from a real landed file, not from memory.** Download the file the old
69
+ integration landed (`dr filebox download <fb> --doc-version <id>`) and profile it: BOM, line endings,
70
+ trailing newline, how empty and null cells are written, when values are quoted, number and boolean
71
+ forms, timestamp formats. The engine's cell writer reproduces exactly that.
72
+ - **Credentials resolve at runtime from the datasource** (`get_credentials`), never from code, tests
73
+ or logs (`tech/security.md` §4). Check a migrated credential's shape without printing it; a stored
74
+ header value may carry its scheme (`Basic <base64>`).
75
+
76
+ ## Before the first merge
77
+
78
+ Run the thin connector's dataquery locally against the live vendor for one closed period and compare
79
+ its bytes with the old integration's landed file for the same period (the `validate-workato-migration`
80
+ skill's `compare_files.py`). Differences only in fields edited after the old run are data drift;
81
+ anything in quoting, format, order or header is a bug to fix before merging. For a refactor, render
82
+ the same fetched records through the old and new code and require identical bytes.
83
+
84
+ ## Common mistakes
85
+
86
+ | Mistake | Fix |
87
+ |---|---|
88
+ | Customer column list or category names as the lib default | Empty default in the lib; the thin file sets them |
89
+ | "A thin connector needs no test" | A thin file that declares columns tests its header and its org-specific sources |
90
+ | One lib connection per scope, named after an output variant (`flat`) | One parameterized connection; the thin file sets `SCOPE` |
91
+ | `csv.writer` / `str(True)` for the file | Write cells the way the landed file shows (`""`, LF, `true`) |
92
+ | Artifact test that lists each client's folder | Discover thin connectors by glob on `from lib.<vendor>.` |
93
+ | A page cap that logs and returns | The cap raises `PermanentError`; test landing exactly on it and exceeding it |
@@ -85,17 +85,46 @@ CONNECTION = "<conn>"
85
85
  ```
86
86
 
87
87
  - **Pick the variant by output, not by customer.** A lib can hold several output shapes of one vendor
88
- (`lib/ramp`: `curated` / `reports` / `flat`). Read the lib's `__init__.py` — it names each variant and
89
- who uses it — and match the columns the customer expects (a Workato migration wants the variant built
90
- for Workato parity). Never merge or edit a variant another org lands data through: that changes their
91
- live output.
88
+ (`lib/ramp`: `curated` / `reports` / `flat` / `pivoted`). Read the lib's `__init__.py` — it names each
89
+ variant and its output — and match the columns the customer expects (a Workato migration wants the
90
+ variant built for Workato parity). Never merge or edit a variant another org lands data through: that
91
+ changes their live output.
92
+ - **Reuse the lib's code before adding any.** A new variant still reuses what the lib already has: the
93
+ helpers in `_core.py` and the lib's one connection. `lib/ramp` has a single
94
+ `connections/base_connection.py`; a thin connection subclasses it and sets `SCOPE`, never a copy. Name a new variant after its output,
95
+ never after the tool or customer it came from. While you're in the lib, list what could be shared or
96
+ cleaned up (a helper two variants duplicate, a pattern that belongs in `_core`) and give that list to
97
+ the user as lib suggestions. Make those changes in their own PR unless the user asks otherwise.
98
+ - **The lib is shared: it never names a client or holds a client's config or data.** No customer or org
99
+ id in its code, docstrings or tests; no client-specific default (a customer's accounting-category
100
+ names, their GL codes); test fixtures are synthetic, never rows copied from a customer's file. Grep the
101
+ lib diff for the customer's name and org id before committing.
92
102
  - **A per-org difference is config or a one-attribute extension, never a copy.** First look for a
93
103
  binding parameter the lib already reads (`lib/ramp`: optional `scope` in `query_config`). Otherwise
94
- subclass the lib's class in the thin connection file and override one attribute —
95
- `samples/sample_lib_connector/` shows both forms.
104
+ subclass the lib's class in the thin connection or dataquery file and override one attribute —
105
+ `samples/sample_lib_connector/` shows the connection form. A dataquery extension also defines its own
106
+ `get_query_class`, keeping `OBJECT_TYPES` one import hop from the lib:
107
+
108
+ ```python
109
+ # <datacenter>/<org_id>_(<org_name>)/<vendor>/dataqueries/<dq>.py
110
+ from dr_integrations_sdk import DataQuery
111
+
112
+ from lib.<vendor>.dataqueries.<variant> import OBJECT_TYPES, TransactionsQuery as _TransactionsQuery # noqa: F401
113
+
114
+ CONNECTION = "<conn>"
115
+
116
+
117
+ class TransactionsQuery(_TransactionsQuery):
118
+ CATEGORIES = ("<the customer's field>", "<another>")
119
+
120
+
121
+ def get_query_class(name: str) -> type[DataQuery]:
122
+ return TransactionsQuery
123
+ ```
96
124
  - **No new test is needed**: a dataquery that only references a lib counts as tested by the lib's own
97
125
  `lib/<vendor>/tests/` (`scripts/check_connector_tests.py`). An extension that adds behaviour tests that
98
- behaviour in the lib or next to the connector.
126
+ behaviour next to the connector (`<vendor>/tests/test_<dq>.py`), since the lib's tests never see the
127
+ customer's values.
99
128
  - **The lib is missing a dataset, or has a bug** → change the lib (with its tests) in its own PR. A lib
100
129
  change republishes every connector that bundles it — new versions, never promoted — so each org moves
101
130
  only when its binding is repointed.
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "dr-cli-integrations": [
3
3
  "connector-sandbox",
4
+ "create-connector-lib",
4
5
  "datarails-connect-workato",
5
6
  "datarails-connectors-onboard-connector",
6
7
  "dr-error-enrichment",
@@ -7,6 +7,7 @@
7
7
  | `dr connectors publish` aborts "published under org A … authenticated as org B" | Passed the customer's dataquery path with `$STG` | Use `<dc>/<staging_org>_(test-workato-migration.com)/…` + `--tag vN` |
8
8
  | Copy workflow: "target exists with DIFFERENT bytes" | A different artifact was already copied for that `vN` | Stop and investigate. Never overwrite: the staging copy must equal what the customer runs |
9
9
  | Second sync of a filebox never runs | The previous run still holds the filebox Singleton | Wait for the version to land before re-triggering |
10
+ | Vendor token call 401 "credentials not found or malformed" with a Workato-sourced credential | The environment property holds the whole header value, scheme included (`Basic <base64>`), because the recipe pastes the pill straight into `Authorization` | Strip the scheme before decoding, then store the decoded parts (e.g. `client_id` / `client_secret`) in the datasource. Check the shape (prefix, lengths) without printing the value |
10
11
  | One row differs, on a recent period only | Vendor data changed after Workato's run | Re-pick older, closed periods before calling it a connector bug |
11
12
  | Only bytes differ, values equal | Quoting / float formatting / line endings | Still a FAIL: match Workato's CSV writer in the connector |
12
13
  | Sync-query shows escaped quotes / doesn't render | `config.query` written as a JSON string | Write it as a nested object; the CLI encodes it |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datarails-ai/dr-cli",
3
- "version": "1.6.80",
3
+ "version": "1.6.82",
4
4
  "description": "Datarails CLI",
5
5
  "type": "module",
6
6
  "engines": {
@@ -17,7 +17,7 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@datarails-ai/dr-appspace-cli": "0.10.4",
20
+ "@datarails-ai/dr-appspace-cli": "0.10.5",
21
21
  "@modelcontextprotocol/sdk": "^1.29.0",
22
22
  "@napi-rs/keyring": "^1.3.0",
23
23
  "commander": "^12.1.0",