@jacebenson/jsn 4.3.0 → 4.3.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/sdk.js +55 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacebenson/jsn",
3
- "version": "4.3.0",
3
+ "version": "4.3.1",
4
4
  "description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/sdk.js CHANGED
@@ -237,12 +237,30 @@ export class SDKClient {
237
237
  * @returns {Promise<object>} created attachment row
238
238
  */
239
239
  async addAttachment(table, sysID, content, fileName) {
240
- const form = new FormData();
241
- const blob = Buffer.isBuffer(content) ? new Blob([content]) : new Blob([String(content)]);
242
- form.append('file', blob, fileName);
243
- const endpoint = `${this.baseURL}/api/now/attachment?table_name=${encodeURIComponent(table)}&table_sys_id=${encodeURIComponent(sysID)}`;
244
- const result = await this.request(endpoint, { method: 'POST', body: form });
245
- return result?.result || null;
240
+ const contentBuffer = Buffer.isBuffer(content) ? content : Buffer.from(String(content));
241
+ const script = [
242
+ '(function() {',
243
+ ` var record = new GlideRecord(${scriptString(table)});`,
244
+ ` if (!record.get(${scriptString(sysID)})) throw new Error('Record not found');`,
245
+ ' var attachment = new GlideSysAttachment();',
246
+ ` var fileName = ${scriptString(fileName)};`,
247
+ ` var content = ${scriptString(contentBuffer.toString('base64'))};`,
248
+ ' var sysId;',
249
+ " if (typeof attachment.writeBase64 === 'function') {",
250
+ " sysId = attachment.writeBase64(record, fileName, 'application/octet-stream', content);",
251
+ ' } else {',
252
+ " sysId = new Attachment().write(record.getTableName(), record.getUniqueValue(), fileName, 'application/octet-stream', GlideStringUtil.base64DecodeAsBytes(content));",
253
+ ' }',
254
+ " if (!sysId) throw new Error('Attachment was not created');",
255
+ " gs.print('JSN_ATTACHMENT_RESULT:' + JSON.stringify({ sys_id: sysId }));",
256
+ '}());',
257
+ ].join('\n');
258
+ const output = await this.executeScript(script, '');
259
+ const result = parseAttachmentResult(output);
260
+ if (!result?.sys_id) {
261
+ throw new Error('Attachment upload returned no attachment sys_id');
262
+ }
263
+ return result;
246
264
  }
247
265
 
248
266
  async getCurrentUser({ touchLastSeen = true } = {}) {
@@ -676,7 +694,10 @@ export class SDKClient {
676
694
  body: formBody.toString(),
677
695
  });
678
696
 
679
- return this._extractScriptOutput(html);
697
+ const output = this._extractScriptOutput(html);
698
+ const scriptError = detectScriptError(output);
699
+ if (scriptError) throw new Error(`Background script failed: ${scriptError}`);
700
+ return output;
680
701
  }
681
702
 
682
703
  /**
@@ -829,6 +850,33 @@ export class SDKClient {
829
850
  }
830
851
  }
831
852
 
853
+ function scriptString(value) {
854
+ return JSON.stringify(String(value)).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
855
+ }
856
+
857
+ function parseAttachmentResult(output) {
858
+ if (!output) return null;
859
+ const lines = String(output).split(/\r?\n/).reverse();
860
+ for (const line of lines) {
861
+ const candidate = line.replace(/^\*\*\* Script:\s*/, '').trim();
862
+ const markerCandidate = candidate.replace(/^JSN_ATTACHMENT_RESULT:\s*/, '');
863
+ if (!markerCandidate.startsWith('{')) continue;
864
+ try {
865
+ const result = JSON.parse(markerCandidate);
866
+ if (result && typeof result === 'object') return result;
867
+ } catch {
868
+ // Ignore unrelated script output and keep looking for the result marker.
869
+ }
870
+ }
871
+ return null;
872
+ }
873
+
874
+ function detectScriptError(output) {
875
+ const lines = String(output || '').split(/\r?\n/).map(line => line.trim()).filter(Boolean);
876
+ const errorLine = lines.find(line => /(?:ReferenceError|TypeError|RhinoEcmaError|Script execution error|undefined is not a function|is not defined)/i.test(line));
877
+ return errorLine || '';
878
+ }
879
+
832
880
  function getBoolField(record, field) {
833
881
  const val = record?.[field];
834
882
  if (val == null) return false;