@tenonhq/dovetail-servicenow 0.0.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 +118 -0
- package/dist/choices.d.ts +17 -0
- package/dist/choices.js +185 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.js +148 -0
- package/dist/client.d.ts +61 -0
- package/dist/client.js +218 -0
- package/dist/formatter.d.ts +6 -0
- package/dist/formatter.js +38 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +17 -0
- package/dist/plugin.d.ts +12 -0
- package/dist/plugin.js +15 -0
- package/dist/types.d.ts +72 -0
- package/dist/types.js +5 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# @tenonhq/dovetail-servicenow
|
|
2
|
+
|
|
3
|
+
ServiceNow platform helpers for Dovetail. The first shipped feature is
|
|
4
|
+
**`addChoicesToField`** — upserts `sys_choice` rows for a given `table.column`
|
|
5
|
+
and flips `sys_dictionary.choice` in one idempotent call, with every write
|
|
6
|
+
captured in the update set you pass in.
|
|
7
|
+
|
|
8
|
+
## Why
|
|
9
|
+
|
|
10
|
+
Adding choice values to a scoped ServiceNow field is a 3-part ritual:
|
|
11
|
+
|
|
12
|
+
1. Find the `sys_dictionary` row for `(table, column)` and set its `choice`
|
|
13
|
+
field (0 = none, 1 = suggestion, **3 = dropdown w/ `-- None --`**).
|
|
14
|
+
2. Create one `sys_choice` row per value/label pair, with `sys_scope` matching
|
|
15
|
+
the dictionary record.
|
|
16
|
+
3. Make sure your user's current update set points at the *right* update set
|
|
17
|
+
(not Default), or the whole change set gets trapped.
|
|
18
|
+
|
|
19
|
+
This package collapses it into:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
await addChoicesToField(client, {
|
|
23
|
+
table: "x_cadso_core_event",
|
|
24
|
+
column: "state",
|
|
25
|
+
updateSetSysId: "0083c3bb33d003507b18bc534d5c7b6d",
|
|
26
|
+
choices: [
|
|
27
|
+
{ value: "delivered", label: "Delivered" },
|
|
28
|
+
{ value: "failed", label: "Failed" }
|
|
29
|
+
]
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Writes go through the **Dovetail Scripted REST API** (`/api/cadso/dovetail/*`,
|
|
34
|
+
historically named "Claude" at `/api/cadso/claude/*`; the client falls back to
|
|
35
|
+
the legacy path on instances where the rename hasn't been imported yet). The
|
|
36
|
+
API pins every write to the supplied update set regardless of the REST user's
|
|
37
|
+
current preference, so re-running with the same inputs is safe — every row
|
|
38
|
+
comes back as `unchanged`.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install @tenonhq/dovetail-servicenow
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Requires Node 20 LTS.
|
|
47
|
+
|
|
48
|
+
## Configure
|
|
49
|
+
|
|
50
|
+
Reads ServiceNow credentials from env vars in this order of precedence:
|
|
51
|
+
|
|
52
|
+
| Field | Preferred | Dev fallback | Prod fallback |
|
|
53
|
+
|----------|-----------------|---------------------|----------------------|
|
|
54
|
+
| Host | `SN_INSTANCE` | `SN_DEV_INSTANCE` | `SN_PROD_INSTANCE` |
|
|
55
|
+
| User | `SN_USER` | `SN_DEV_USERNAME` | `SN_PROD_USERNAME` |
|
|
56
|
+
| Password | `SN_PASSWORD` | `SN_DEV_PASSWORD` | `SN_PROD_PASSWORD` |
|
|
57
|
+
|
|
58
|
+
The dev/prod fallbacks match the names already documented in
|
|
59
|
+
`Craftsman/CLAUDE.local.md`, so existing developer setups work out of the box.
|
|
60
|
+
Bare instance names (e.g. `TenonWorkStudio`) get `.service-now.com` appended
|
|
61
|
+
automatically.
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
SN_INSTANCE=tenonworkstudio.service-now.com
|
|
65
|
+
SN_USER=...
|
|
66
|
+
SN_PASSWORD=...
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## CLI
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Inline form
|
|
73
|
+
npx sinc-sn add-choices \
|
|
74
|
+
--table x_cadso_core_event \
|
|
75
|
+
--column state \
|
|
76
|
+
--update-set 0083c3bb33d003507b18bc534d5c7b6d \
|
|
77
|
+
--choices "delivered=Delivered,failed=Failed,expired=Expired"
|
|
78
|
+
|
|
79
|
+
# JSON payload form (recommended for >5 choices)
|
|
80
|
+
npx sinc-sn add-choices --from-json ./choices.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
JSON payload shape:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"table": "x_cadso_core_event",
|
|
88
|
+
"column": "state",
|
|
89
|
+
"updateSetSysId": "0083c3bb33d003507b18bc534d5c7b6d",
|
|
90
|
+
"choiceType": 3,
|
|
91
|
+
"choices": [
|
|
92
|
+
{ "value": "delivered", "label": "Delivered" },
|
|
93
|
+
{ "value": "failed", "label": "Failed" }
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Programmatic
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { createClient, addChoicesToField } from "@tenonhq/dovetail-servicenow";
|
|
102
|
+
|
|
103
|
+
var client = createClient({});
|
|
104
|
+
var result = await addChoicesToField(client, { /* ... */ });
|
|
105
|
+
|
|
106
|
+
console.log(result.choices);
|
|
107
|
+
// [
|
|
108
|
+
// { value: "delivered", label: "Delivered", sysId: "...", action: "created" },
|
|
109
|
+
// { value: "failed", label: "Failed", sysId: "...", action: "created" }
|
|
110
|
+
// ]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Roadmap
|
|
114
|
+
|
|
115
|
+
Same package will grow to cover the rest of the `sinch-dlr-manual-steps`
|
|
116
|
+
patterns: indexes (`sys_db_object_ix`), table properties (`accessible_from`),
|
|
117
|
+
`sys_trigger` creation, `sys_property` creation. Pattern stays identical —
|
|
118
|
+
query to diff, write through the Dovetail REST API, report per-row actions.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* addChoicesToField — upsert sys_choice rows for a given table.column,
|
|
3
|
+
* and (optionally) flip sys_dictionary.choice so the column renders as a dropdown.
|
|
4
|
+
*
|
|
5
|
+
* All writes go through the Dovetail "Claude" Scripted REST API, which pins
|
|
6
|
+
* each write to the supplied update set regardless of the REST user's current
|
|
7
|
+
* preference. sys_scope on sys_choice is inherited from the dictionary record
|
|
8
|
+
* so choices stay in the same application as the field.
|
|
9
|
+
*/
|
|
10
|
+
import type { ServiceNowClient } from "./client";
|
|
11
|
+
import type { AddChoicesParams, AddChoicesResult } from "./types";
|
|
12
|
+
/**
|
|
13
|
+
* Upsert choices for a field and (optionally) toggle sys_dictionary.choice.
|
|
14
|
+
* Idempotent: re-running with the same inputs returns `action: "unchanged"`
|
|
15
|
+
* for every row and skips the dictionary write when no change is required.
|
|
16
|
+
*/
|
|
17
|
+
export declare function addChoicesToField(client: ServiceNowClient, params: AddChoicesParams): Promise<AddChoicesResult>;
|
package/dist/choices.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* addChoicesToField — upsert sys_choice rows for a given table.column,
|
|
4
|
+
* and (optionally) flip sys_dictionary.choice so the column renders as a dropdown.
|
|
5
|
+
*
|
|
6
|
+
* All writes go through the Dovetail "Claude" Scripted REST API, which pins
|
|
7
|
+
* each write to the supplied update set regardless of the REST user's current
|
|
8
|
+
* preference. sys_scope on sys_choice is inherited from the dictionary record
|
|
9
|
+
* so choices stay in the same application as the field.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.addChoicesToField = addChoicesToField;
|
|
13
|
+
function encodeQueryValue(v) {
|
|
14
|
+
// ServiceNow encoded-query values: commas/carets/equals are special. We
|
|
15
|
+
// don't expect them in table/column/value/language inputs, but keep this
|
|
16
|
+
// escape-lite to surface surprises loudly rather than silently.
|
|
17
|
+
if (/[,\^=]/.test(v)) {
|
|
18
|
+
throw new Error("Invalid character in query value: " + JSON.stringify(v));
|
|
19
|
+
}
|
|
20
|
+
return v;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a sys_scope record's namespace name (e.g. "x_cadso_core") from its
|
|
24
|
+
* sys_id. The Claude REST API's `scope` parameter expects the namespace name,
|
|
25
|
+
* not the sys_id, so dictionary records (whose sys_scope is a sys_id) need
|
|
26
|
+
* a translation step before we can pass them through.
|
|
27
|
+
*/
|
|
28
|
+
async function resolveScopeName(client, scopeSysId) {
|
|
29
|
+
if (!scopeSysId)
|
|
30
|
+
return "";
|
|
31
|
+
var rows = await client.table.query("sys_scope", "sys_id=" + encodeQueryValue(scopeSysId), 1);
|
|
32
|
+
if (rows.length === 0) {
|
|
33
|
+
throw new Error("sys_scope record not found for sys_id " + scopeSysId);
|
|
34
|
+
}
|
|
35
|
+
// sys_scope.scope is the namespace (e.g. "x_cadso_core"); fall back to name
|
|
36
|
+
// if a scope record was created without a populated scope field.
|
|
37
|
+
return rows[0].scope || rows[0].name || "";
|
|
38
|
+
}
|
|
39
|
+
async function fetchDictionary(client, table, column) {
|
|
40
|
+
var rows = await client.table.query("sys_dictionary", "name=" + encodeQueryValue(table) + "^element=" + encodeQueryValue(column), 1);
|
|
41
|
+
if (rows.length === 0) {
|
|
42
|
+
throw new Error("sys_dictionary record not found for " + table + "." + column +
|
|
43
|
+
" — verify the field exists and your user has read access.");
|
|
44
|
+
}
|
|
45
|
+
var row = rows[0];
|
|
46
|
+
// sys_scope comes back as a reference object or string depending on display_value.
|
|
47
|
+
// We set sysparm_display_value=false in client.query so we get the sys_id string.
|
|
48
|
+
var scope = typeof row.sys_scope === "object" && row.sys_scope != null
|
|
49
|
+
? row.sys_scope.value
|
|
50
|
+
: row.sys_scope;
|
|
51
|
+
return {
|
|
52
|
+
sys_id: row.sys_id,
|
|
53
|
+
name: row.name,
|
|
54
|
+
element: row.element,
|
|
55
|
+
choice: String(row.choice || "0"),
|
|
56
|
+
sys_scope: scope || ""
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function fetchUpdateSet(client, sysId) {
|
|
60
|
+
var rows = await client.table.query("sys_update_set", "sys_id=" + encodeQueryValue(sysId), 1);
|
|
61
|
+
if (rows.length === 0) {
|
|
62
|
+
throw new Error("Update set " + sysId + " not found — verify the sys_id and your access.");
|
|
63
|
+
}
|
|
64
|
+
var row = rows[0];
|
|
65
|
+
if (row.state && row.state !== "in progress" && row.state !== "in_progress") {
|
|
66
|
+
throw new Error("Update set " + row.name + " is in state '" + row.state +
|
|
67
|
+
"' — only 'in progress' update sets can capture new changes.");
|
|
68
|
+
}
|
|
69
|
+
return row;
|
|
70
|
+
}
|
|
71
|
+
async function fetchExistingChoices(client, table, column) {
|
|
72
|
+
return client.table.query("sys_choice", "name=" + encodeQueryValue(table) +
|
|
73
|
+
"^element=" + encodeQueryValue(column), 1000);
|
|
74
|
+
}
|
|
75
|
+
function buildChoiceFields(table, column, choice, scope) {
|
|
76
|
+
var fields = {
|
|
77
|
+
name: table,
|
|
78
|
+
element: column,
|
|
79
|
+
value: choice.value,
|
|
80
|
+
label: choice.label,
|
|
81
|
+
language: choice.language || "en",
|
|
82
|
+
inactive: "false"
|
|
83
|
+
};
|
|
84
|
+
if (choice.sequence != null) {
|
|
85
|
+
fields.sequence = String(choice.sequence);
|
|
86
|
+
}
|
|
87
|
+
if (scope) {
|
|
88
|
+
fields.sys_scope = scope;
|
|
89
|
+
}
|
|
90
|
+
return fields;
|
|
91
|
+
}
|
|
92
|
+
function isUnchanged(existing, choice) {
|
|
93
|
+
var sameLabel = existing.label === choice.label;
|
|
94
|
+
var sameLang = (existing.language || "en") === (choice.language || "en");
|
|
95
|
+
var sameSeq = choice.sequence == null
|
|
96
|
+
? true
|
|
97
|
+
: String(existing.sequence || "") === String(choice.sequence);
|
|
98
|
+
return sameLabel && sameLang && sameSeq && existing.inactive === "false";
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Upsert choices for a field and (optionally) toggle sys_dictionary.choice.
|
|
102
|
+
* Idempotent: re-running with the same inputs returns `action: "unchanged"`
|
|
103
|
+
* for every row and skips the dictionary write when no change is required.
|
|
104
|
+
*/
|
|
105
|
+
async function addChoicesToField(client, params) {
|
|
106
|
+
if (!params.updateSetSysId) {
|
|
107
|
+
throw new Error("updateSetSysId is required — every write must be captured in a named update set.");
|
|
108
|
+
}
|
|
109
|
+
if (!params.choices || params.choices.length === 0) {
|
|
110
|
+
throw new Error("choices must be a non-empty array.");
|
|
111
|
+
}
|
|
112
|
+
var dict = await fetchDictionary(client, params.table, params.column);
|
|
113
|
+
var updateSet = await fetchUpdateSet(client, params.updateSetSysId);
|
|
114
|
+
var scopeName = await resolveScopeName(client, dict.sys_scope);
|
|
115
|
+
var targetChoiceType = params.choiceType === null
|
|
116
|
+
? Number(dict.choice)
|
|
117
|
+
: (params.choiceType != null ? params.choiceType : 3);
|
|
118
|
+
var choiceWas = Number(dict.choice);
|
|
119
|
+
var choiceNow = choiceWas;
|
|
120
|
+
if (params.choiceType !== null && Number(dict.choice) !== targetChoiceType) {
|
|
121
|
+
await client.claude.pushWithUpdateSet({
|
|
122
|
+
update_set_sys_id: params.updateSetSysId,
|
|
123
|
+
table: "sys_dictionary",
|
|
124
|
+
record_sys_id: dict.sys_id,
|
|
125
|
+
fields: { choice: String(targetChoiceType) }
|
|
126
|
+
});
|
|
127
|
+
choiceNow = targetChoiceType;
|
|
128
|
+
}
|
|
129
|
+
var existing = await fetchExistingChoices(client, params.table, params.column);
|
|
130
|
+
var existingByValue = {};
|
|
131
|
+
existing.forEach(function (row) {
|
|
132
|
+
var key = (row.language || "en") + "::" + row.value;
|
|
133
|
+
existingByValue[key] = row;
|
|
134
|
+
});
|
|
135
|
+
var results = [];
|
|
136
|
+
for (var i = 0; i < params.choices.length; i += 1) {
|
|
137
|
+
var choice = params.choices[i];
|
|
138
|
+
var key = (choice.language || "en") + "::" + choice.value;
|
|
139
|
+
var match = existingByValue[key];
|
|
140
|
+
if (match && isUnchanged(match, choice)) {
|
|
141
|
+
results.push({ value: choice.value, label: choice.label, sysId: match.sys_id, action: "unchanged" });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (match) {
|
|
145
|
+
var updFields = {
|
|
146
|
+
label: choice.label,
|
|
147
|
+
language: choice.language || "en",
|
|
148
|
+
inactive: "false"
|
|
149
|
+
};
|
|
150
|
+
if (choice.sequence != null) {
|
|
151
|
+
updFields.sequence = String(choice.sequence);
|
|
152
|
+
}
|
|
153
|
+
await client.claude.pushWithUpdateSet({
|
|
154
|
+
update_set_sys_id: params.updateSetSysId,
|
|
155
|
+
table: "sys_choice",
|
|
156
|
+
record_sys_id: match.sys_id,
|
|
157
|
+
fields: updFields
|
|
158
|
+
});
|
|
159
|
+
results.push({ value: choice.value, label: choice.label, sysId: match.sys_id, action: "updated" });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
var created = await client.claude.createRecord({
|
|
163
|
+
table: "sys_choice",
|
|
164
|
+
fields: buildChoiceFields(params.table, params.column, choice, dict.sys_scope),
|
|
165
|
+
scope: scopeName,
|
|
166
|
+
update_set_sys_id: params.updateSetSysId
|
|
167
|
+
});
|
|
168
|
+
results.push({
|
|
169
|
+
value: choice.value,
|
|
170
|
+
label: choice.label,
|
|
171
|
+
sysId: created.sys_id,
|
|
172
|
+
action: "created"
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
dictionary: {
|
|
177
|
+
sysId: dict.sys_id,
|
|
178
|
+
scope: dict.sys_scope,
|
|
179
|
+
choiceWas: choiceWas,
|
|
180
|
+
choiceNow: choiceNow
|
|
181
|
+
},
|
|
182
|
+
updateSet: { sysId: updateSet.sys_id, name: updateSet.name },
|
|
183
|
+
choices: results
|
|
184
|
+
};
|
|
185
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* sinc-sn — thin CLI adapter for @tenonhq/dovetail-servicenow.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* sinc-sn add-choices \
|
|
7
|
+
* --table x_cadso_core_event \
|
|
8
|
+
* --column state \
|
|
9
|
+
* --update-set <sys_id> \
|
|
10
|
+
* --choices 'delivered=Delivered,failed=Failed,...' \
|
|
11
|
+
* [--choice-type 3] [--json]
|
|
12
|
+
*
|
|
13
|
+
* sinc-sn add-choices --from-json path/to/choices.json
|
|
14
|
+
*
|
|
15
|
+
* JSON payload shape:
|
|
16
|
+
* {
|
|
17
|
+
* "table": "x_cadso_core_event",
|
|
18
|
+
* "column": "state",
|
|
19
|
+
* "updateSetSysId": "...",
|
|
20
|
+
* "choiceType": 3,
|
|
21
|
+
* "choices": [{ "value": "delivered", "label": "Delivered" }, ...]
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* sinc-sn — thin CLI adapter for @tenonhq/dovetail-servicenow.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* sinc-sn add-choices \
|
|
8
|
+
* --table x_cadso_core_event \
|
|
9
|
+
* --column state \
|
|
10
|
+
* --update-set <sys_id> \
|
|
11
|
+
* --choices 'delivered=Delivered,failed=Failed,...' \
|
|
12
|
+
* [--choice-type 3] [--json]
|
|
13
|
+
*
|
|
14
|
+
* sinc-sn add-choices --from-json path/to/choices.json
|
|
15
|
+
*
|
|
16
|
+
* JSON payload shape:
|
|
17
|
+
* {
|
|
18
|
+
* "table": "x_cadso_core_event",
|
|
19
|
+
* "column": "state",
|
|
20
|
+
* "updateSetSysId": "...",
|
|
21
|
+
* "choiceType": 3,
|
|
22
|
+
* "choices": [{ "value": "delivered", "label": "Delivered" }, ...]
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
26
|
+
if (k2 === undefined) k2 = k;
|
|
27
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
28
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
29
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
30
|
+
}
|
|
31
|
+
Object.defineProperty(o, k2, desc);
|
|
32
|
+
}) : (function(o, m, k, k2) {
|
|
33
|
+
if (k2 === undefined) k2 = k;
|
|
34
|
+
o[k2] = m[k];
|
|
35
|
+
}));
|
|
36
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
37
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
38
|
+
}) : function(o, v) {
|
|
39
|
+
o["default"] = v;
|
|
40
|
+
});
|
|
41
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
42
|
+
var ownKeys = function(o) {
|
|
43
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
44
|
+
var ar = [];
|
|
45
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
46
|
+
return ar;
|
|
47
|
+
};
|
|
48
|
+
return ownKeys(o);
|
|
49
|
+
};
|
|
50
|
+
return function (mod) {
|
|
51
|
+
if (mod && mod.__esModule) return mod;
|
|
52
|
+
var result = {};
|
|
53
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
54
|
+
__setModuleDefault(result, mod);
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
})();
|
|
58
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
59
|
+
const fs = __importStar(require("fs"));
|
|
60
|
+
const client_1 = require("./client");
|
|
61
|
+
const choices_1 = require("./choices");
|
|
62
|
+
const formatter_1 = require("./formatter");
|
|
63
|
+
function parseArgs(argv) {
|
|
64
|
+
var command = argv[0] || "";
|
|
65
|
+
var flags = {};
|
|
66
|
+
for (var i = 1; i < argv.length; i += 1) {
|
|
67
|
+
var arg = argv[i];
|
|
68
|
+
if (arg.indexOf("--") !== 0)
|
|
69
|
+
continue;
|
|
70
|
+
var key = arg.slice(2);
|
|
71
|
+
var value = "true";
|
|
72
|
+
var eq = key.indexOf("=");
|
|
73
|
+
if (eq !== -1) {
|
|
74
|
+
value = key.slice(eq + 1);
|
|
75
|
+
key = key.slice(0, eq);
|
|
76
|
+
}
|
|
77
|
+
else if (i + 1 < argv.length && argv[i + 1].indexOf("--") !== 0) {
|
|
78
|
+
value = argv[i + 1];
|
|
79
|
+
i += 1;
|
|
80
|
+
}
|
|
81
|
+
flags[key] = value;
|
|
82
|
+
}
|
|
83
|
+
return { command: command, flags: flags };
|
|
84
|
+
}
|
|
85
|
+
function parseChoicesInline(input) {
|
|
86
|
+
return input.split(",").map(function (pair) {
|
|
87
|
+
var parts = pair.split("=");
|
|
88
|
+
if (parts.length !== 2) {
|
|
89
|
+
throw new Error("Invalid --choices entry '" + pair + "' (expected value=Label)");
|
|
90
|
+
}
|
|
91
|
+
return { value: parts[0].trim(), label: parts[1].trim() };
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function paramsFromFlags(flags) {
|
|
95
|
+
if (flags["from-json"]) {
|
|
96
|
+
var raw = fs.readFileSync(flags["from-json"], "utf8");
|
|
97
|
+
var obj = JSON.parse(raw);
|
|
98
|
+
return obj;
|
|
99
|
+
}
|
|
100
|
+
var table = flags.table;
|
|
101
|
+
var column = flags.column;
|
|
102
|
+
var updateSetSysId = flags["update-set"] || flags.updateSetSysId;
|
|
103
|
+
var choicesInline = flags.choices;
|
|
104
|
+
if (!table || !column || !updateSetSysId || !choicesInline) {
|
|
105
|
+
throw new Error("Missing required flags: --table, --column, --update-set, --choices");
|
|
106
|
+
}
|
|
107
|
+
var params = {
|
|
108
|
+
table: table,
|
|
109
|
+
column: column,
|
|
110
|
+
updateSetSysId: updateSetSysId,
|
|
111
|
+
choices: parseChoicesInline(choicesInline)
|
|
112
|
+
};
|
|
113
|
+
if (flags["choice-type"]) {
|
|
114
|
+
params.choiceType = Number(flags["choice-type"]);
|
|
115
|
+
}
|
|
116
|
+
return params;
|
|
117
|
+
}
|
|
118
|
+
async function runAddChoices(flags) {
|
|
119
|
+
var params = paramsFromFlags(flags);
|
|
120
|
+
var client = (0, client_1.createClient)({});
|
|
121
|
+
var result = await (0, choices_1.addChoicesToField)(client, params);
|
|
122
|
+
if (flags.json === "true") {
|
|
123
|
+
process.stdout.write(JSON.stringify({ params: params, result: result }, null, 2) + "\n");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
process.stdout.write((0, formatter_1.formatAddChoicesResult)(params.table, params.column, result) + "\n");
|
|
127
|
+
}
|
|
128
|
+
function printHelp() {
|
|
129
|
+
process.stdout.write("sinc-sn — ServiceNow helpers\n\n" +
|
|
130
|
+
"Commands:\n" +
|
|
131
|
+
" add-choices Upsert sys_choice rows for a table.column (see --help in source)\n");
|
|
132
|
+
}
|
|
133
|
+
async function main() {
|
|
134
|
+
var parsed = parseArgs(process.argv.slice(2));
|
|
135
|
+
if (parsed.command === "add-choices") {
|
|
136
|
+
await runAddChoices(parsed.flags);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!parsed.command || parsed.command === "help" || parsed.flags.help === "true") {
|
|
140
|
+
printHelp();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
throw new Error("Unknown command: " + parsed.command);
|
|
144
|
+
}
|
|
145
|
+
main().catch(function (err) {
|
|
146
|
+
process.stderr.write("sinc-sn error: " + (err && err.message ? err.message : String(err)) + "\n");
|
|
147
|
+
process.exit(1);
|
|
148
|
+
});
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ServiceNow REST client for @tenonhq/dovetail-servicenow.
|
|
3
|
+
*
|
|
4
|
+
* Provides two entry points:
|
|
5
|
+
* - `table.*` — read-only GETs against the native Table API
|
|
6
|
+
* - `claude.*` — writes via the Dovetail Scripted REST API
|
|
7
|
+
* (/api/cadso/dovetail/*), which handles update-set + scope
|
|
8
|
+
* switching atomically so every write lands in the right
|
|
9
|
+
* update set without touching sys_user_preference.
|
|
10
|
+
* Falls back to the legacy /api/cadso/claude/* path on
|
|
11
|
+
* instances where the API has not yet been re-imported.
|
|
12
|
+
* The namespace name `claude` is preserved for API
|
|
13
|
+
* compatibility; the underlying server-side API is now
|
|
14
|
+
* named "Dovetail".
|
|
15
|
+
*
|
|
16
|
+
* Env fallbacks mirror prior dashboard-fetch helpers so dev setups that already
|
|
17
|
+
* have SN_INSTANCE/SN_USER/SN_PASSWORD work without reconfiguration.
|
|
18
|
+
*/
|
|
19
|
+
import type { ServiceNowClientConfig } from "./types";
|
|
20
|
+
export interface TableQueryOptions {
|
|
21
|
+
limit?: number;
|
|
22
|
+
fields?: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface ServiceNowClient {
|
|
25
|
+
table: {
|
|
26
|
+
/** GET /api/now/table/<t>?sysparm_query=...&sysparm_limit=N — returns result array. */
|
|
27
|
+
query: {
|
|
28
|
+
<T = Record<string, any>>(table: string, query: string, limit?: number): Promise<Array<T>>;
|
|
29
|
+
<T = Record<string, any>>(table: string, query: string, options: TableQueryOptions): Promise<Array<T>>;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
claude: {
|
|
33
|
+
/** POST /api/cadso/dovetail/createRecord (legacy path: /api/cadso/claude/createRecord). */
|
|
34
|
+
createRecord: (params: {
|
|
35
|
+
table: string;
|
|
36
|
+
fields: Record<string, any>;
|
|
37
|
+
scope?: string;
|
|
38
|
+
update_set_sys_id?: string;
|
|
39
|
+
sys_id?: string;
|
|
40
|
+
}) => Promise<{
|
|
41
|
+
sys_id: string;
|
|
42
|
+
[k: string]: any;
|
|
43
|
+
}>;
|
|
44
|
+
/** POST /api/cadso/dovetail/pushWithUpdateSet (legacy: /api/cadso/claude/pushWithUpdateSet). */
|
|
45
|
+
pushWithUpdateSet: (params: {
|
|
46
|
+
update_set_sys_id: string;
|
|
47
|
+
table: string;
|
|
48
|
+
record_sys_id: string;
|
|
49
|
+
fields: Record<string, any>;
|
|
50
|
+
}) => Promise<{
|
|
51
|
+
sys_id: string;
|
|
52
|
+
[k: string]: any;
|
|
53
|
+
}>;
|
|
54
|
+
/** GET /api/cadso/dovetail/currentUpdateSet?scope=... (legacy: /api/cadso/claude/currentUpdateSet). */
|
|
55
|
+
currentUpdateSet: (scope?: string) => Promise<{
|
|
56
|
+
sys_id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
}>;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export declare function createClient(config?: ServiceNowClientConfig): ServiceNowClient;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ServiceNow REST client for @tenonhq/dovetail-servicenow.
|
|
4
|
+
*
|
|
5
|
+
* Provides two entry points:
|
|
6
|
+
* - `table.*` — read-only GETs against the native Table API
|
|
7
|
+
* - `claude.*` — writes via the Dovetail Scripted REST API
|
|
8
|
+
* (/api/cadso/dovetail/*), which handles update-set + scope
|
|
9
|
+
* switching atomically so every write lands in the right
|
|
10
|
+
* update set without touching sys_user_preference.
|
|
11
|
+
* Falls back to the legacy /api/cadso/claude/* path on
|
|
12
|
+
* instances where the API has not yet been re-imported.
|
|
13
|
+
* The namespace name `claude` is preserved for API
|
|
14
|
+
* compatibility; the underlying server-side API is now
|
|
15
|
+
* named "Dovetail".
|
|
16
|
+
*
|
|
17
|
+
* Env fallbacks mirror prior dashboard-fetch helpers so dev setups that already
|
|
18
|
+
* have SN_INSTANCE/SN_USER/SN_PASSWORD work without reconfiguration.
|
|
19
|
+
*/
|
|
20
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.createClient = createClient;
|
|
25
|
+
const axios_1 = __importDefault(require("axios"));
|
|
26
|
+
/**
|
|
27
|
+
* Env precedence for instance/auth: explicit cfg > SN_* > SN_DEV_* > SN_PROD_*.
|
|
28
|
+
* SN_DEV_* / SN_PROD_* fallbacks match the names documented in Craftsman/CLAUDE.local.md
|
|
29
|
+
* so existing developer setups work without re-exporting variables. SN_DEV_INSTANCE
|
|
30
|
+
* may be a bare instance name (e.g. "TenonWorkStudio") — `.service-now.com` is
|
|
31
|
+
* appended when it isn't already part of the host.
|
|
32
|
+
*/
|
|
33
|
+
function normalizeHost(raw) {
|
|
34
|
+
var host = raw.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
35
|
+
if (host && host.indexOf(".") === -1) {
|
|
36
|
+
host = host.toLowerCase() + ".service-now.com";
|
|
37
|
+
}
|
|
38
|
+
return host;
|
|
39
|
+
}
|
|
40
|
+
function resolveInstance(cfg) {
|
|
41
|
+
var raw = cfg.instance
|
|
42
|
+
|| process.env.SN_INSTANCE
|
|
43
|
+
|| process.env.SN_DEV_INSTANCE
|
|
44
|
+
|| process.env.SN_PROD_INSTANCE
|
|
45
|
+
|| "";
|
|
46
|
+
if (!raw) {
|
|
47
|
+
throw new Error("ServiceNow instance not configured. Set SN_INSTANCE (preferred) or SN_DEV_INSTANCE / SN_PROD_INSTANCE, or pass { instance }.");
|
|
48
|
+
}
|
|
49
|
+
return normalizeHost(raw);
|
|
50
|
+
}
|
|
51
|
+
function resolveAuth(cfg) {
|
|
52
|
+
var user = cfg.user
|
|
53
|
+
|| process.env.SN_USER
|
|
54
|
+
|| process.env.SN_DEV_USERNAME
|
|
55
|
+
|| process.env.SN_PROD_USERNAME
|
|
56
|
+
|| "";
|
|
57
|
+
var password = cfg.password
|
|
58
|
+
|| process.env.SN_PASSWORD
|
|
59
|
+
|| process.env.SN_DEV_PASSWORD
|
|
60
|
+
|| process.env.SN_PROD_PASSWORD
|
|
61
|
+
|| "";
|
|
62
|
+
if (!user || !password) {
|
|
63
|
+
throw new Error("ServiceNow credentials missing — set SN_USER/SN_PASSWORD (preferred) " +
|
|
64
|
+
"or SN_DEV_USERNAME/SN_DEV_PASSWORD (or SN_PROD_*).");
|
|
65
|
+
}
|
|
66
|
+
return { user: user, password: password };
|
|
67
|
+
}
|
|
68
|
+
function sleep(ms) {
|
|
69
|
+
return new Promise(function (resolve) {
|
|
70
|
+
setTimeout(resolve, ms);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function createClient(config = {}) {
|
|
74
|
+
var host = resolveInstance(config);
|
|
75
|
+
var creds = resolveAuth(config);
|
|
76
|
+
var intervalMs = config.requestIntervalMs != null
|
|
77
|
+
? config.requestIntervalMs
|
|
78
|
+
: Number(process.env.SN_REQUEST_INTERVAL_MS) || 20;
|
|
79
|
+
var max429 = config.maxRetries429 != null
|
|
80
|
+
? config.maxRetries429
|
|
81
|
+
: Number(process.env.SN_MAX_RETRIES_429) || 5;
|
|
82
|
+
var max5xx = config.maxRetries5xx != null
|
|
83
|
+
? config.maxRetries5xx
|
|
84
|
+
: Number(process.env.SN_MAX_RETRIES_5XX) || 3;
|
|
85
|
+
var http = axios_1.default.create({
|
|
86
|
+
baseURL: "https://" + host,
|
|
87
|
+
auth: { username: creds.user, password: creds.password },
|
|
88
|
+
headers: { accept: "application/json", "content-type": "application/json" },
|
|
89
|
+
validateStatus: function () { return true; }
|
|
90
|
+
});
|
|
91
|
+
var lastAt = 0;
|
|
92
|
+
// Dovetail Scripted REST API rebrand: prefer /api/cadso/dovetail/* and fall back
|
|
93
|
+
// to the legacy /api/cadso/claude/* path on instances where the rename hasn't
|
|
94
|
+
// been imported yet. Latch the legacy flag after the first 404 to avoid paying
|
|
95
|
+
// the round-trip cost on every subsequent call.
|
|
96
|
+
var useDovetailLegacyClaudePath = false;
|
|
97
|
+
async function request(cfg, ctx) {
|
|
98
|
+
var attempt429 = 0;
|
|
99
|
+
var attempt5xx = 0;
|
|
100
|
+
// eslint-disable-next-line no-constant-condition
|
|
101
|
+
while (true) {
|
|
102
|
+
var elapsed = Date.now() - lastAt;
|
|
103
|
+
if (elapsed < intervalMs) {
|
|
104
|
+
await sleep(intervalMs - elapsed);
|
|
105
|
+
}
|
|
106
|
+
lastAt = Date.now();
|
|
107
|
+
var res;
|
|
108
|
+
try {
|
|
109
|
+
res = await http.request(cfg);
|
|
110
|
+
}
|
|
111
|
+
catch (netErr) {
|
|
112
|
+
if (attempt5xx >= max5xx) {
|
|
113
|
+
throw new Error("SN network error on " + ctx + ": " + (netErr && netErr.message));
|
|
114
|
+
}
|
|
115
|
+
attempt5xx += 1;
|
|
116
|
+
await sleep(Math.pow(2, attempt5xx) * 1000);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (res.status === 401 || res.status === 403) {
|
|
120
|
+
throw new Error("SN auth error " + res.status + " on " + ctx + " — check SN_USER/SN_PASSWORD and ACLs.");
|
|
121
|
+
}
|
|
122
|
+
if (res.status === 404) {
|
|
123
|
+
throw new Error("SN 404 on " + ctx + " — endpoint or record not found.");
|
|
124
|
+
}
|
|
125
|
+
if (res.status === 429) {
|
|
126
|
+
if (attempt429 >= max429) {
|
|
127
|
+
throw new Error("SN 429 rate limit — retries exhausted on " + ctx);
|
|
128
|
+
}
|
|
129
|
+
attempt429 += 1;
|
|
130
|
+
await sleep(Math.min(60000, Math.pow(2, attempt429) * 1000));
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (res.status >= 500) {
|
|
134
|
+
if (attempt5xx >= max5xx) {
|
|
135
|
+
throw new Error("SN " + res.status + " on " + ctx + " — retries exhausted.");
|
|
136
|
+
}
|
|
137
|
+
attempt5xx += 1;
|
|
138
|
+
await sleep(Math.pow(2, attempt5xx) * 1000);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (res.status < 200 || res.status >= 300) {
|
|
142
|
+
var body = typeof res.data === "string" ? res.data : JSON.stringify(res.data);
|
|
143
|
+
throw new Error("SN " + res.status + " on " + ctx + ": " + body.substring(0, 400));
|
|
144
|
+
}
|
|
145
|
+
return res.data;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Dovetail Scripted REST API request: try /api/cadso/dovetail/<op>, fall back to
|
|
149
|
+
// /api/cadso/claude/<op> on 404 (with a one-time deprecation warning).
|
|
150
|
+
async function dovetailRequest(method, op, body, params, ctx) {
|
|
151
|
+
var url = useDovetailLegacyClaudePath
|
|
152
|
+
? "/api/cadso/claude/" + op
|
|
153
|
+
: "/api/cadso/dovetail/" + op;
|
|
154
|
+
try {
|
|
155
|
+
return await request({ method: method, url: url, data: body, params: params }, ctx);
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
var msg = e && e.message ? String(e.message) : "";
|
|
159
|
+
if (!useDovetailLegacyClaudePath && msg.indexOf("SN 404 on") === 0) {
|
|
160
|
+
// eslint-disable-next-line no-console
|
|
161
|
+
console.warn("[deprecation] /api/cadso/dovetail/" + op +
|
|
162
|
+
" returned 404. Falling back to legacy /api/cadso/claude/" + op +
|
|
163
|
+
". Re-import the Dovetail Scripted REST API XML on your ServiceNow instance to silence this warning.");
|
|
164
|
+
useDovetailLegacyClaudePath = true;
|
|
165
|
+
var legacyUrl = "/api/cadso/claude/" + op;
|
|
166
|
+
return await request({ method: method, url: legacyUrl, data: body, params: params }, ctx);
|
|
167
|
+
}
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
table: {
|
|
173
|
+
query: async function (table, query, limitOrOptions) {
|
|
174
|
+
var limit = 100;
|
|
175
|
+
var fields;
|
|
176
|
+
if (typeof limitOrOptions === "number") {
|
|
177
|
+
limit = limitOrOptions;
|
|
178
|
+
}
|
|
179
|
+
else if (limitOrOptions && typeof limitOrOptions === "object") {
|
|
180
|
+
if (typeof limitOrOptions.limit === "number") {
|
|
181
|
+
limit = limitOrOptions.limit;
|
|
182
|
+
}
|
|
183
|
+
if (limitOrOptions.fields && limitOrOptions.fields.length > 0) {
|
|
184
|
+
fields = limitOrOptions.fields;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
var params = {
|
|
188
|
+
sysparm_query: query,
|
|
189
|
+
sysparm_limit: limit,
|
|
190
|
+
sysparm_display_value: false
|
|
191
|
+
};
|
|
192
|
+
if (fields) {
|
|
193
|
+
params.sysparm_fields = fields.join(",");
|
|
194
|
+
}
|
|
195
|
+
var data = await request({
|
|
196
|
+
method: "GET",
|
|
197
|
+
url: "/api/now/table/" + encodeURIComponent(table),
|
|
198
|
+
params: params
|
|
199
|
+
}, "table.query(" + table + ")");
|
|
200
|
+
return data.result || [];
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
claude: {
|
|
204
|
+
createRecord: async function (params) {
|
|
205
|
+
var data = await dovetailRequest("POST", "createRecord", params, null, "claude.createRecord(" + params.table + ")");
|
|
206
|
+
return data.result || data;
|
|
207
|
+
},
|
|
208
|
+
pushWithUpdateSet: async function (params) {
|
|
209
|
+
var data = await dovetailRequest("POST", "pushWithUpdateSet", params, null, "claude.pushWithUpdateSet(" + params.table + ")");
|
|
210
|
+
return data.result || data;
|
|
211
|
+
},
|
|
212
|
+
currentUpdateSet: async function (scope) {
|
|
213
|
+
var data = await dovetailRequest("GET", "currentUpdateSet", null, scope ? { scope: scope } : null, "claude.currentUpdateSet");
|
|
214
|
+
return data.result || data;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AddChoicesResult } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Human-readable one-page summary of an addChoicesToField result.
|
|
4
|
+
* Used by the CLI and by Claude skills when surfacing outcomes back to the user.
|
|
5
|
+
*/
|
|
6
|
+
export declare function formatAddChoicesResult(table: string, column: string, result: AddChoicesResult): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatAddChoicesResult = formatAddChoicesResult;
|
|
4
|
+
/**
|
|
5
|
+
* Human-readable one-page summary of an addChoicesToField result.
|
|
6
|
+
* Used by the CLI and by Claude skills when surfacing outcomes back to the user.
|
|
7
|
+
*/
|
|
8
|
+
function formatAddChoicesResult(table, column, result) {
|
|
9
|
+
var lines = [];
|
|
10
|
+
lines.push("ServiceNow choice values — " + table + "." + column);
|
|
11
|
+
lines.push("");
|
|
12
|
+
lines.push("Update set: " + result.updateSet.name + " (" + result.updateSet.sysId + ")");
|
|
13
|
+
lines.push("Dictionary: " + result.dictionary.sysId + " [scope " + result.dictionary.scope + "]");
|
|
14
|
+
if (result.dictionary.choiceWas !== result.dictionary.choiceNow) {
|
|
15
|
+
lines.push(" sys_dictionary.choice: " + result.dictionary.choiceWas + " -> " + result.dictionary.choiceNow);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
lines.push(" sys_dictionary.choice: " + result.dictionary.choiceNow + " (unchanged)");
|
|
19
|
+
}
|
|
20
|
+
lines.push("");
|
|
21
|
+
var created = 0;
|
|
22
|
+
var updated = 0;
|
|
23
|
+
var unchanged = 0;
|
|
24
|
+
lines.push("Choices:");
|
|
25
|
+
result.choices.forEach(function (row) {
|
|
26
|
+
if (row.action === "created")
|
|
27
|
+
created += 1;
|
|
28
|
+
else if (row.action === "updated")
|
|
29
|
+
updated += 1;
|
|
30
|
+
else
|
|
31
|
+
unchanged += 1;
|
|
32
|
+
lines.push(" [" + row.action.padEnd(9) + "] " + row.value + " -> " + row.label +
|
|
33
|
+
" (" + row.sysId + ")");
|
|
34
|
+
});
|
|
35
|
+
lines.push("");
|
|
36
|
+
lines.push("Summary: " + created + " created, " + updated + " updated, " + unchanged + " unchanged.");
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tenonhq/dovetail-servicenow
|
|
3
|
+
*
|
|
4
|
+
* ServiceNow helpers that route writes through the Dovetail "Claude" Scripted
|
|
5
|
+
* REST API so every change lands in the target update set and scope.
|
|
6
|
+
*/
|
|
7
|
+
export { createClient } from "./client";
|
|
8
|
+
export type { ServiceNowClient, TableQueryOptions } from "./client";
|
|
9
|
+
export { addChoicesToField } from "./choices";
|
|
10
|
+
export { formatAddChoicesResult } from "./formatter";
|
|
11
|
+
export { sincPlugin } from "./plugin";
|
|
12
|
+
export type { ServiceNowClientConfig, ChoiceValue, ChoiceType, AddChoicesParams, AddChoicesResult, ChoiceActionResult, DictionaryRecord, UpdateSetRecord } from "./types";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @tenonhq/dovetail-servicenow
|
|
4
|
+
*
|
|
5
|
+
* ServiceNow helpers that route writes through the Dovetail "Claude" Scripted
|
|
6
|
+
* REST API so every change lands in the target update set and scope.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.sincPlugin = exports.formatAddChoicesResult = exports.addChoicesToField = exports.createClient = void 0;
|
|
10
|
+
var client_1 = require("./client");
|
|
11
|
+
Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
|
|
12
|
+
var choices_1 = require("./choices");
|
|
13
|
+
Object.defineProperty(exports, "addChoicesToField", { enumerable: true, get: function () { return choices_1.addChoicesToField; } });
|
|
14
|
+
var formatter_1 = require("./formatter");
|
|
15
|
+
Object.defineProperty(exports, "formatAddChoicesResult", { enumerable: true, get: function () { return formatter_1.formatAddChoicesResult; } });
|
|
16
|
+
var plugin_1 = require("./plugin");
|
|
17
|
+
Object.defineProperty(exports, "sincPlugin", { enumerable: true, get: function () { return plugin_1.sincPlugin; } });
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dovetail init plugin for @tenonhq/dovetail-servicenow.
|
|
3
|
+
* Auth piggybacks on `npx dove configure` — SN_INSTANCE / SN_USER / SN_PASSWORD
|
|
4
|
+
* already get wired there, so this plugin is currently a no-op discoverable marker.
|
|
5
|
+
*/
|
|
6
|
+
export declare const sincPlugin: {
|
|
7
|
+
name: string;
|
|
8
|
+
displayName: string;
|
|
9
|
+
description: string;
|
|
10
|
+
login: never[];
|
|
11
|
+
configure: never[];
|
|
12
|
+
};
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Dovetail init plugin for @tenonhq/dovetail-servicenow.
|
|
4
|
+
* Auth piggybacks on `npx dove configure` — SN_INSTANCE / SN_USER / SN_PASSWORD
|
|
5
|
+
* already get wired there, so this plugin is currently a no-op discoverable marker.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.sincPlugin = void 0;
|
|
9
|
+
exports.sincPlugin = {
|
|
10
|
+
name: "servicenow",
|
|
11
|
+
displayName: "ServiceNow",
|
|
12
|
+
description: "Dictionary / choice helpers and update-set-aware writes",
|
|
13
|
+
login: [],
|
|
14
|
+
configure: []
|
|
15
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @tenonhq/dovetail-servicenow — type definitions
|
|
3
|
+
*/
|
|
4
|
+
export interface ServiceNowClientConfig {
|
|
5
|
+
/** Instance host, e.g. "tenonworkstudio.service-now.com". Defaults to SN_INSTANCE env var. */
|
|
6
|
+
instance?: string;
|
|
7
|
+
/** Basic-auth user. Defaults to SN_USER env var. */
|
|
8
|
+
user?: string;
|
|
9
|
+
/** Basic-auth password. Defaults to SN_PASSWORD env var. */
|
|
10
|
+
password?: string;
|
|
11
|
+
/** Min gap between requests (ms). Defaults to SN_REQUEST_INTERVAL_MS or 20. */
|
|
12
|
+
requestIntervalMs?: number;
|
|
13
|
+
/** Max retries on 429. Defaults to SN_MAX_RETRIES_429 or 5. */
|
|
14
|
+
maxRetries429?: number;
|
|
15
|
+
/** Max retries on 5xx/network. Defaults to SN_MAX_RETRIES_5XX or 3. */
|
|
16
|
+
maxRetries5xx?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface ChoiceValue {
|
|
19
|
+
value: string;
|
|
20
|
+
label: string;
|
|
21
|
+
/** Order hint. Optional; ServiceNow auto-sequences when omitted. */
|
|
22
|
+
sequence?: number;
|
|
23
|
+
/** Defaults to "en". */
|
|
24
|
+
language?: string;
|
|
25
|
+
}
|
|
26
|
+
/** sys_dictionary.choice column values. 0 = none, 1 = suggestion, 3 = dropdown w/ --None--. */
|
|
27
|
+
export type ChoiceType = 0 | 1 | 3;
|
|
28
|
+
export interface AddChoicesParams {
|
|
29
|
+
/** Target table, e.g. "x_cadso_core_event". */
|
|
30
|
+
table: string;
|
|
31
|
+
/** Target column, e.g. "state". */
|
|
32
|
+
column: string;
|
|
33
|
+
/** Choice values to upsert. */
|
|
34
|
+
choices: Array<ChoiceValue>;
|
|
35
|
+
/** Update set sys_id that will capture every write. Required — no default. */
|
|
36
|
+
updateSetSysId: string;
|
|
37
|
+
/** sys_dictionary.choice setting. Defaults to 3 (dropdown). Pass null to leave dictionary alone. */
|
|
38
|
+
choiceType?: ChoiceType | null;
|
|
39
|
+
}
|
|
40
|
+
export interface DictionaryRecord {
|
|
41
|
+
sys_id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
element: string;
|
|
44
|
+
choice: string;
|
|
45
|
+
/** sys_scope is a reference; ServiceNow returns the sys_id string. */
|
|
46
|
+
sys_scope: string;
|
|
47
|
+
}
|
|
48
|
+
export interface UpdateSetRecord {
|
|
49
|
+
sys_id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
state: string;
|
|
52
|
+
application: string;
|
|
53
|
+
}
|
|
54
|
+
export interface ChoiceActionResult {
|
|
55
|
+
value: string;
|
|
56
|
+
label: string;
|
|
57
|
+
sysId: string;
|
|
58
|
+
action: "created" | "updated" | "unchanged";
|
|
59
|
+
}
|
|
60
|
+
export interface AddChoicesResult {
|
|
61
|
+
dictionary: {
|
|
62
|
+
sysId: string;
|
|
63
|
+
scope: string;
|
|
64
|
+
choiceWas: ChoiceType;
|
|
65
|
+
choiceNow: ChoiceType;
|
|
66
|
+
};
|
|
67
|
+
updateSet: {
|
|
68
|
+
sysId: string;
|
|
69
|
+
name: string;
|
|
70
|
+
};
|
|
71
|
+
choices: Array<ChoiceActionResult>;
|
|
72
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tenonhq/dovetail-servicenow",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "ServiceNow platform helpers for Dovetail — dictionary, choices, update-set-aware writes via the Dovetail Scripted REST API",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dove-sn": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "jest",
|
|
11
|
+
"prepack": "tsc",
|
|
12
|
+
"version:bump": "node ./../../Scripts/bump-version.js ./package.json",
|
|
13
|
+
"postpublish": "npm run version:bump"
|
|
14
|
+
},
|
|
15
|
+
"author": "Tenon",
|
|
16
|
+
"license": "GPL-3.0",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"axios": "^1.5.1"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/jest": "^29.5.5",
|
|
22
|
+
"@types/node": ">=20.8.4",
|
|
23
|
+
"jest": "^29.7.0",
|
|
24
|
+
"ts-jest": "^29.1.1",
|
|
25
|
+
"typescript": "^5.2.2"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/tenonhq/dovetail.git"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/tenonhq/dovetail/issues"
|
|
39
|
+
}
|
|
40
|
+
}
|