@openfn/language-mssql 6.0.6 → 7.0.0

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/ast.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "params"
7
7
  ],
8
8
  "docs": {
9
- "description": "Execute an SQL statement",
9
+ "description": "Execute a raw SQL statement",
10
10
  "tags": [
11
11
  {
12
12
  "title": "public",
@@ -15,7 +15,7 @@
15
15
  },
16
16
  {
17
17
  "title": "example",
18
- "description": "sql({ query, options })"
18
+ "description": "sql({\n query: 'SELECT * FROM users WHERE id = @id',\n options\n})"
19
19
  },
20
20
  {
21
21
  "title": "function",
@@ -24,13 +24,31 @@
24
24
  },
25
25
  {
26
26
  "title": "param",
27
- "description": "Payload data for the message",
27
+ "description": "Payload data for the SQL query",
28
28
  "type": {
29
29
  "type": "NameExpression",
30
30
  "name": "object"
31
31
  },
32
32
  "name": "params"
33
33
  },
34
+ {
35
+ "title": "param",
36
+ "description": "The SQL query to execute",
37
+ "type": {
38
+ "type": "NameExpression",
39
+ "name": "string"
40
+ },
41
+ "name": "params.query"
42
+ },
43
+ {
44
+ "title": "param",
45
+ "description": "Optional query options",
46
+ "type": {
47
+ "type": "NameExpression",
48
+ "name": "object"
49
+ },
50
+ "name": "params.options"
51
+ },
34
52
  {
35
53
  "title": "returns",
36
54
  "description": null,
@@ -41,7 +59,7 @@
41
59
  }
42
60
  ]
43
61
  },
44
- "valid": true
62
+ "valid": false
45
63
  },
46
64
  {
47
65
  "name": "findValue",
@@ -57,9 +57,10 @@
57
57
  },
58
58
  "trustServerCertificate": {
59
59
  "type": "boolean",
60
- "default": true,
60
+ "default": false,
61
+ "description": "Trust self-signed certificates (not recommended for production)",
61
62
  "examples": [
62
- false
63
+ true
63
64
  ]
64
65
  }
65
66
  },
package/dist/index.cjs CHANGED
@@ -86,15 +86,73 @@ var import_util = require("@openfn/language-common/util");
86
86
  // src/util.js
87
87
  var util_exports = {};
88
88
  __export(util_exports, {
89
- escape: () => escape
89
+ escape: () => escape,
90
+ escapeIdentifier: () => escapeIdentifier,
91
+ escapeLike: () => escapeLike,
92
+ validateOperator: () => validateOperator
90
93
  });
91
94
  function escape(stringExp) {
92
95
  if (typeof stringExp === "object" && stringExp !== null) {
96
+ if (Array.isArray(stringExp)) {
97
+ return stringExp.map((x) => escape(x));
98
+ }
93
99
  return Object.values(stringExp).map((x) => escape(x));
94
- } else if (typeof stringExp !== "string") {
100
+ }
101
+ if (stringExp === null || stringExp === void 0) {
95
102
  return stringExp;
96
103
  }
97
- return stringExp.replace(/\'/g, "''");
104
+ if (typeof stringExp === "number" || typeof stringExp === "boolean") {
105
+ return String(stringExp);
106
+ }
107
+ if (typeof stringExp !== "string") {
108
+ throw new Error(`Unexpected type for SQL value: ${typeof stringExp}`);
109
+ }
110
+ return stringExp.replace(/'/g, "''");
111
+ }
112
+ function escapeLike(pattern) {
113
+ let escaped = escape(pattern);
114
+ escaped = escaped.replace(/\[/g, "[[[]");
115
+ escaped = escaped.replace(/%/g, "[%]");
116
+ escaped = escaped.replace(/_/g, "[_]");
117
+ return escaped;
118
+ }
119
+ function escapeIdentifier(identifier) {
120
+ if (typeof identifier !== "string" || identifier.length === 0) {
121
+ throw new Error("Identifier must be a non-empty string");
122
+ }
123
+ return `[${identifier.replace(/\]/g, "]]")}]`;
124
+ }
125
+ var ALLOWED_OPERATORS = [
126
+ "=",
127
+ "!=",
128
+ "<>",
129
+ "<",
130
+ ">",
131
+ "<=",
132
+ ">=",
133
+ "LIKE",
134
+ "NOT LIKE",
135
+ "IN",
136
+ "NOT IN",
137
+ "IS NULL",
138
+ "IS NOT NULL"
139
+ ];
140
+ function validateOperator(operator) {
141
+ if (typeof operator !== "string") {
142
+ throw new Error("Operator must be a non-empty string");
143
+ }
144
+ const normalized = operator.trim().toUpperCase();
145
+ if (normalized.length === 0) {
146
+ throw new Error("Operator must be a non-empty string");
147
+ }
148
+ if (!ALLOWED_OPERATORS.includes(normalized)) {
149
+ throw new Error(
150
+ `Invalid SQL operator: "${operator}". Allowed operators: ${ALLOWED_OPERATORS.join(
151
+ ", "
152
+ )}`
153
+ );
154
+ }
155
+ return normalized;
98
156
  }
99
157
 
100
158
  // src/Adaptor.js
@@ -108,7 +166,7 @@ function createConnection(state) {
108
166
  database,
109
167
  port = 1433,
110
168
  encrypt = true,
111
- trustServerCertificate = true
169
+ trustServerCertificate = false
112
170
  } = state.configuration;
113
171
  if (!server) {
114
172
  throw new Error("server missing from configuration.");
@@ -160,9 +218,10 @@ function cleanupState(state) {
160
218
  state == null ? true : delete state.connection;
161
219
  return state;
162
220
  }
163
- function addRowsToRefs(state, rows) {
221
+ function addRowsToRefs(state, rows, rowCount) {
164
222
  return {
165
223
  ...state,
224
+ rowCount,
166
225
  references: [rows, ...state.references]
167
226
  };
168
227
  }
@@ -209,7 +268,7 @@ function queryHandler(state, query, callback, options) {
209
268
  return;
210
269
  } else {
211
270
  console.log(`Finished: ${rowCount} row(s).`);
212
- resolve(callback(state, rows));
271
+ resolve(callback(state, rows, rowCount));
213
272
  }
214
273
  });
215
274
  connection.execSql(request);
@@ -221,6 +280,20 @@ function sql(params) {
221
280
  try {
222
281
  const [resolvedParams] = (0, import_util.expandReferences)(state, params);
223
282
  const { query, options } = resolvedParams;
283
+ const dangerousPatterns = [
284
+ /;\s*DROP\s+TABLE/i,
285
+ /;\s*DELETE\s+FROM/i,
286
+ /;\s*TRUNCATE/i,
287
+ /--\s*$/m,
288
+ /\/\*.*\*\//
289
+ ];
290
+ dangerousPatterns.forEach((pattern) => {
291
+ if (pattern.test(query)) {
292
+ throw new Error(
293
+ "SQL query contains potentially dangerous pattern. Ensure this is intentional and from a trusted source."
294
+ );
295
+ }
296
+ });
224
297
  console.log(`Preparing to execute sql statement: ${query}`);
225
298
  return queryHandler(state, query, composeNextState, options);
226
299
  } catch (e) {
@@ -259,16 +332,21 @@ function findValue(filter) {
259
332
  const { connection } = state;
260
333
  const { uuid, relation, where, operator } = filter;
261
334
  const [whereData, operatorData] = (0, import_util.expandReferences)(state, where, operator);
335
+ const safeUuid = escapeIdentifier(uuid);
336
+ const safeRelation = escapeIdentifier(relation);
262
337
  let conditionsArray = [];
263
338
  for (let key in whereData) {
264
- const escapedValue = escape(whereData[key]);
339
+ const safeColumnName = escapeIdentifier(key);
340
+ const validatedOperator = operatorData ? validateOperator(operatorData[key]) : "=";
341
+ const isLikeOperator = validatedOperator === "LIKE" || validatedOperator === "NOT LIKE";
342
+ const escapedValue = isLikeOperator ? escapeLike(whereData[key]) : escape(whereData[key]);
265
343
  conditionsArray.push(
266
- `${key} ${operatorData ? operatorData[key] : "="} '${escapedValue}'`
344
+ `${safeColumnName} ${validatedOperator} '${escapedValue}'`
267
345
  );
268
346
  }
269
347
  const condition = conditionsArray.length > 0 ? `where ${conditionsArray.join(" and ")}` : "";
270
348
  try {
271
- const body = `select ${uuid} from ${relation} ${condition}`;
349
+ const body = `select ${safeUuid} from ${safeRelation} ${condition}`;
272
350
  console.log("Preparing to execute sql statement");
273
351
  let returnValue = null;
274
352
  return new Promise((resolve, reject) => {
@@ -301,13 +379,17 @@ function insert(table, record, options) {
301
379
  const { connection } = state;
302
380
  try {
303
381
  const [recordData] = (0, import_util.expandReferences)(state, record);
382
+ const safeTable = escapeIdentifier(table);
304
383
  const columns = Object.keys(recordData).sort();
384
+ const safeColumns = columns.map((col) => escapeIdentifier(col));
305
385
  const values = columns.map((key) => escape(recordData[key])).join("', '");
306
386
  const query = handleValues(
307
- `INSERT INTO ${table} (${columns.join(", ")}) VALUES ('${values}');`,
387
+ `INSERT INTO ${safeTable} (${safeColumns.join(
388
+ ", "
389
+ )}) VALUES ('${values}');`,
308
390
  handleOptions(options)
309
391
  );
310
- const safeQuery = `INSERT INTO ${table} (${columns.join(
392
+ const safeQuery = `INSERT INTO ${safeTable} (${safeColumns.join(
311
393
  ", "
312
394
  )}) VALUES [--REDACTED--]];`;
313
395
  return new Promise((resolve) => {
@@ -326,17 +408,19 @@ function insertMany(table, records, options) {
326
408
  const { connection } = state;
327
409
  try {
328
410
  const [recordData] = (0, import_util.expandReferences)(state, records);
411
+ const safeTable = escapeIdentifier(table);
329
412
  const columns = Object.keys(recordData[0]);
413
+ const safeColumns = columns.map((col) => escapeIdentifier(col));
330
414
  const valueSets = recordData.map(
331
415
  (x) => `('${escape(Object.values(x)).join("', '")}')`
332
416
  );
333
417
  const query = handleValues(
334
- `INSERT INTO ${table} (${columns.join(", ")}) VALUES ${valueSets.join(
418
+ `INSERT INTO ${safeTable} (${safeColumns.join(
335
419
  ", "
336
- )};`,
420
+ )}) VALUES ${valueSets.join(", ")};`,
337
421
  handleOptions(options)
338
422
  );
339
- const safeQuery = `INSERT INTO ${table} (${columns.join(
423
+ const safeQuery = `INSERT INTO ${safeTable} (${safeColumns.join(
340
424
  ", "
341
425
  )}) VALUES [--REDACTED--]];`;
342
426
  return new Promise((resolve) => {
@@ -355,21 +439,27 @@ function upsert(table, uuid, record, options) {
355
439
  const { connection } = state;
356
440
  try {
357
441
  const [recordData] = (0, import_util.expandReferences)(state, record);
442
+ const safeTable = escapeIdentifier(table);
358
443
  const columns = Object.keys(recordData).sort();
359
- const selectValues = columns.map((key) => `'${escape(recordData[key])}' AS ${key}`).join(", ");
360
- const updateValues = columns.map((key) => `[Target].${key}='${escape(recordData[key])}'`).join(", ");
361
- const insertColumns = columns.join(", ");
362
- const insertValues = columns.map((key) => `[Source].${key}`).join(", ");
444
+ const safeColumns = columns.map((col) => escapeIdentifier(col));
445
+ const selectValues = columns.map((key, i) => `'${escape(recordData[key])}' AS ${safeColumns[i]}`).join(", ");
446
+ const updateValues = columns.map(
447
+ (key, i) => `[Target].${safeColumns[i]}='${escape(recordData[key])}'`
448
+ ).join(", ");
449
+ const insertColumns = safeColumns.join(", ");
450
+ const insertValues = safeColumns.map((col) => `[Source].${col}`).join(", ");
363
451
  const constraint = [];
364
452
  if (Array.isArray(uuid)) {
365
453
  uuid.forEach((key) => {
366
- constraint.push(`[Target].${key} = [Source].${key}`);
454
+ const safeKey = escapeIdentifier(key);
455
+ constraint.push(`[Target].${safeKey} = [Source].${safeKey}`);
367
456
  });
368
457
  } else {
369
- constraint.push(`[Target].${uuid} = [Source].${uuid}`);
458
+ const safeUuid = escapeIdentifier(uuid);
459
+ constraint.push(`[Target].${safeUuid} = [Source].${safeUuid}`);
370
460
  }
371
461
  const query = handleValues(
372
- `MERGE ${table} AS [Target]
462
+ `MERGE ${safeTable} AS [Target]
373
463
  USING (SELECT ${selectValues}) AS [Source]
374
464
  ON ${constraint.join(" AND ")}
375
465
  WHEN MATCHED THEN
@@ -378,7 +468,7 @@ function upsert(table, uuid, record, options) {
378
468
  INSERT (${insertColumns}) VALUES (${insertValues});`,
379
469
  handleOptions(options)
380
470
  );
381
- const safeQuery = `MERGE ${table} AS [Target]
471
+ const safeQuery = `MERGE ${safeTable} AS [Target]
382
472
  USING (SELECT [--REDACTED--])
383
473
  ON [Target].[--VALUE--] = [Source].[--VALUE--]
384
474
  WHEN MATCHED THEN
@@ -458,23 +548,27 @@ function upsertMany(table, uuid, records, options) {
458
548
  console.log("No records provided; skipping upsert.");
459
549
  resolve(state);
460
550
  }
551
+ const safeTable = escapeIdentifier(table);
461
552
  const columns = Object.keys(recordData[0]);
553
+ const safeColumns = columns.map((col) => escapeIdentifier(col));
462
554
  const valueSets = recordData.map(
463
555
  (x) => `('${escape(Object.values(x)).join("', '")}')`
464
556
  );
465
- const insertColumns = columns.join(", ");
466
- const insertValues = columns.map((key) => `[Source].${key}`).join(", ");
467
- const updateValues = columns.map((key) => `[Target].${key}=[Source].${key}`).join(", ");
557
+ const insertColumns = safeColumns.join(", ");
558
+ const insertValues = safeColumns.map((col) => `[Source].${col}`).join(", ");
559
+ const updateValues = safeColumns.map((col) => `[Target].${col}=[Source].${col}`).join(", ");
468
560
  const constraint = [];
469
561
  if (Array.isArray(uuid)) {
470
562
  uuid.forEach((key) => {
471
- constraint.push(`[Target].${key} = [Source].${key}`);
563
+ const safeKey = escapeIdentifier(key);
564
+ constraint.push(`[Target].${safeKey} = [Source].${safeKey}`);
472
565
  });
473
566
  } else {
474
- constraint.push(`[Target].${uuid} = [Source].${uuid}`);
567
+ const safeUuid = escapeIdentifier(uuid);
568
+ constraint.push(`[Target].${safeUuid} = [Source].${safeUuid}`);
475
569
  }
476
570
  const query = handleValues(
477
- `MERGE ${table} AS [Target]
571
+ `MERGE ${safeTable} AS [Target]
478
572
  USING (VALUES ${valueSets.join(", ")}) AS [Source] (${insertColumns})
479
573
  ON ${constraint.join(" AND ")}
480
574
  WHEN MATCHED THEN
@@ -483,7 +577,7 @@ function upsertMany(table, uuid, records, options) {
483
577
  INSERT (${insertColumns}) VALUES (${insertValues});`,
484
578
  handleOptions(options)
485
579
  );
486
- const safeQuery = `MERGE ${table} AS [Target]
580
+ const safeQuery = `MERGE ${safeTable} AS [Target]
487
581
  USING (VALUES [--REDACTED--]) AS [SOURCE] (${insertColumns})
488
582
  ON [Target].[--VALUE--] = [Source].[--VALUE--]
489
583
  WHEN MATCHED THEN
@@ -528,10 +622,12 @@ function insertTable(tableName, columns, options) {
528
622
  resolve(state);
529
623
  return state;
530
624
  }
531
- const structureData = data.map(
532
- (x) => `${x.name} ${x.type} ${x.hasOwnProperty("default") ? x.type.includes("varchar") || x.type.includes("text") ? `DEFAULT '${x.default}'` : `DEFAULT ${x.default}` : ""} ${x.unique ? "UNIQUE" : ""} ${x.identity ? "PRIMARY KEY IDENTITY (1,1)" : ""} ${x.required ? "NOT NULL" : ""}`
533
- ).join(", ");
534
- const query = `CREATE TABLE ${tableName} (
625
+ const safeTableName = escapeIdentifier(tableName);
626
+ const structureData = data.map((x) => {
627
+ const safeColName = escapeIdentifier(x.name);
628
+ return `${safeColName} ${x.type} ${x.hasOwnProperty("default") ? x.type.includes("varchar") || x.type.includes("text") ? `DEFAULT '${escape(x.default)}'` : `DEFAULT ${x.default}` : ""} ${x.unique ? "UNIQUE" : ""} ${x.identity ? "PRIMARY KEY IDENTITY (1,1)" : ""} ${x.required ? "NOT NULL" : ""}`;
629
+ }).join(", ");
630
+ const query = `CREATE TABLE ${safeTableName} (
535
631
  ${structureData}
536
632
  );`;
537
633
  console.log("Preparing to create table via:", query);
@@ -554,10 +650,12 @@ function modifyTable(tableName, columns, options) {
554
650
  resolve(state);
555
651
  return state;
556
652
  }
557
- const structureData = data.map(
558
- (x) => `${x.name} ${x.type} ${x.hasOwnProperty("default") ? x.type.includes("varchar") || x.type.includes("text") ? `DEFAULT '${x.default}'` : `DEFAULT ${x.default}` : ""} ${x.unique ? "UNIQUE" : ""} ${x.identity ? "IDENTITY (1,1)" : ""} ${x.required ? "NOT NULL" : ""}`
559
- ).join(", ");
560
- const query = `ALTER TABLE ${tableName} ADD ${structureData};`;
653
+ const safeTableName = escapeIdentifier(tableName);
654
+ const structureData = data.map((x) => {
655
+ const safeColName = escapeIdentifier(x.name);
656
+ return `${safeColName} ${x.type} ${x.hasOwnProperty("default") ? x.type.includes("varchar") || x.type.includes("text") ? `DEFAULT '${escape(x.default)}'` : `DEFAULT ${x.default}` : ""} ${x.unique ? "UNIQUE" : ""} ${x.identity ? "IDENTITY (1,1)" : ""} ${x.required ? "NOT NULL" : ""}`;
657
+ }).join(", ");
658
+ const query = `ALTER TABLE ${safeTableName} ADD ${structureData};`;
561
659
  console.log("Preparing to modify table via:", query);
562
660
  resolve(queryHandler(state, query, flattenRows, options));
563
661
  });
package/dist/index.js CHANGED
@@ -40,15 +40,73 @@ import { expandReferences } from "@openfn/language-common/util";
40
40
  // src/util.js
41
41
  var util_exports = {};
42
42
  __export(util_exports, {
43
- escape: () => escape
43
+ escape: () => escape,
44
+ escapeIdentifier: () => escapeIdentifier,
45
+ escapeLike: () => escapeLike,
46
+ validateOperator: () => validateOperator
44
47
  });
45
48
  function escape(stringExp) {
46
49
  if (typeof stringExp === "object" && stringExp !== null) {
50
+ if (Array.isArray(stringExp)) {
51
+ return stringExp.map((x) => escape(x));
52
+ }
47
53
  return Object.values(stringExp).map((x) => escape(x));
48
- } else if (typeof stringExp !== "string") {
54
+ }
55
+ if (stringExp === null || stringExp === void 0) {
49
56
  return stringExp;
50
57
  }
51
- return stringExp.replace(/\'/g, "''");
58
+ if (typeof stringExp === "number" || typeof stringExp === "boolean") {
59
+ return String(stringExp);
60
+ }
61
+ if (typeof stringExp !== "string") {
62
+ throw new Error(`Unexpected type for SQL value: ${typeof stringExp}`);
63
+ }
64
+ return stringExp.replace(/'/g, "''");
65
+ }
66
+ function escapeLike(pattern) {
67
+ let escaped = escape(pattern);
68
+ escaped = escaped.replace(/\[/g, "[[[]");
69
+ escaped = escaped.replace(/%/g, "[%]");
70
+ escaped = escaped.replace(/_/g, "[_]");
71
+ return escaped;
72
+ }
73
+ function escapeIdentifier(identifier) {
74
+ if (typeof identifier !== "string" || identifier.length === 0) {
75
+ throw new Error("Identifier must be a non-empty string");
76
+ }
77
+ return `[${identifier.replace(/\]/g, "]]")}]`;
78
+ }
79
+ var ALLOWED_OPERATORS = [
80
+ "=",
81
+ "!=",
82
+ "<>",
83
+ "<",
84
+ ">",
85
+ "<=",
86
+ ">=",
87
+ "LIKE",
88
+ "NOT LIKE",
89
+ "IN",
90
+ "NOT IN",
91
+ "IS NULL",
92
+ "IS NOT NULL"
93
+ ];
94
+ function validateOperator(operator) {
95
+ if (typeof operator !== "string") {
96
+ throw new Error("Operator must be a non-empty string");
97
+ }
98
+ const normalized = operator.trim().toUpperCase();
99
+ if (normalized.length === 0) {
100
+ throw new Error("Operator must be a non-empty string");
101
+ }
102
+ if (!ALLOWED_OPERATORS.includes(normalized)) {
103
+ throw new Error(
104
+ `Invalid SQL operator: "${operator}". Allowed operators: ${ALLOWED_OPERATORS.join(
105
+ ", "
106
+ )}`
107
+ );
108
+ }
109
+ return normalized;
52
110
  }
53
111
 
54
112
  // src/Adaptor.js
@@ -78,7 +136,7 @@ function createConnection(state) {
78
136
  database,
79
137
  port = 1433,
80
138
  encrypt = true,
81
- trustServerCertificate = true
139
+ trustServerCertificate = false
82
140
  } = state.configuration;
83
141
  if (!server) {
84
142
  throw new Error("server missing from configuration.");
@@ -130,9 +188,10 @@ function cleanupState(state) {
130
188
  state == null ? true : delete state.connection;
131
189
  return state;
132
190
  }
133
- function addRowsToRefs(state, rows) {
191
+ function addRowsToRefs(state, rows, rowCount) {
134
192
  return {
135
193
  ...state,
194
+ rowCount,
136
195
  references: [rows, ...state.references]
137
196
  };
138
197
  }
@@ -179,7 +238,7 @@ function queryHandler(state, query, callback, options) {
179
238
  return;
180
239
  } else {
181
240
  console.log(`Finished: ${rowCount} row(s).`);
182
- resolve(callback(state, rows));
241
+ resolve(callback(state, rows, rowCount));
183
242
  }
184
243
  });
185
244
  connection.execSql(request);
@@ -191,6 +250,20 @@ function sql(params) {
191
250
  try {
192
251
  const [resolvedParams] = expandReferences(state, params);
193
252
  const { query, options } = resolvedParams;
253
+ const dangerousPatterns = [
254
+ /;\s*DROP\s+TABLE/i,
255
+ /;\s*DELETE\s+FROM/i,
256
+ /;\s*TRUNCATE/i,
257
+ /--\s*$/m,
258
+ /\/\*.*\*\//
259
+ ];
260
+ dangerousPatterns.forEach((pattern) => {
261
+ if (pattern.test(query)) {
262
+ throw new Error(
263
+ "SQL query contains potentially dangerous pattern. Ensure this is intentional and from a trusted source."
264
+ );
265
+ }
266
+ });
194
267
  console.log(`Preparing to execute sql statement: ${query}`);
195
268
  return queryHandler(state, query, composeNextState, options);
196
269
  } catch (e) {
@@ -229,16 +302,21 @@ function findValue(filter) {
229
302
  const { connection } = state;
230
303
  const { uuid, relation, where, operator } = filter;
231
304
  const [whereData, operatorData] = expandReferences(state, where, operator);
305
+ const safeUuid = escapeIdentifier(uuid);
306
+ const safeRelation = escapeIdentifier(relation);
232
307
  let conditionsArray = [];
233
308
  for (let key in whereData) {
234
- const escapedValue = escape(whereData[key]);
309
+ const safeColumnName = escapeIdentifier(key);
310
+ const validatedOperator = operatorData ? validateOperator(operatorData[key]) : "=";
311
+ const isLikeOperator = validatedOperator === "LIKE" || validatedOperator === "NOT LIKE";
312
+ const escapedValue = isLikeOperator ? escapeLike(whereData[key]) : escape(whereData[key]);
235
313
  conditionsArray.push(
236
- `${key} ${operatorData ? operatorData[key] : "="} '${escapedValue}'`
314
+ `${safeColumnName} ${validatedOperator} '${escapedValue}'`
237
315
  );
238
316
  }
239
317
  const condition = conditionsArray.length > 0 ? `where ${conditionsArray.join(" and ")}` : "";
240
318
  try {
241
- const body = `select ${uuid} from ${relation} ${condition}`;
319
+ const body = `select ${safeUuid} from ${safeRelation} ${condition}`;
242
320
  console.log("Preparing to execute sql statement");
243
321
  let returnValue = null;
244
322
  return new Promise((resolve, reject) => {
@@ -271,13 +349,17 @@ function insert(table, record, options) {
271
349
  const { connection } = state;
272
350
  try {
273
351
  const [recordData] = expandReferences(state, record);
352
+ const safeTable = escapeIdentifier(table);
274
353
  const columns = Object.keys(recordData).sort();
354
+ const safeColumns = columns.map((col) => escapeIdentifier(col));
275
355
  const values = columns.map((key) => escape(recordData[key])).join("', '");
276
356
  const query = handleValues(
277
- `INSERT INTO ${table} (${columns.join(", ")}) VALUES ('${values}');`,
357
+ `INSERT INTO ${safeTable} (${safeColumns.join(
358
+ ", "
359
+ )}) VALUES ('${values}');`,
278
360
  handleOptions(options)
279
361
  );
280
- const safeQuery = `INSERT INTO ${table} (${columns.join(
362
+ const safeQuery = `INSERT INTO ${safeTable} (${safeColumns.join(
281
363
  ", "
282
364
  )}) VALUES [--REDACTED--]];`;
283
365
  return new Promise((resolve) => {
@@ -296,17 +378,19 @@ function insertMany(table, records, options) {
296
378
  const { connection } = state;
297
379
  try {
298
380
  const [recordData] = expandReferences(state, records);
381
+ const safeTable = escapeIdentifier(table);
299
382
  const columns = Object.keys(recordData[0]);
383
+ const safeColumns = columns.map((col) => escapeIdentifier(col));
300
384
  const valueSets = recordData.map(
301
385
  (x) => `('${escape(Object.values(x)).join("', '")}')`
302
386
  );
303
387
  const query = handleValues(
304
- `INSERT INTO ${table} (${columns.join(", ")}) VALUES ${valueSets.join(
388
+ `INSERT INTO ${safeTable} (${safeColumns.join(
305
389
  ", "
306
- )};`,
390
+ )}) VALUES ${valueSets.join(", ")};`,
307
391
  handleOptions(options)
308
392
  );
309
- const safeQuery = `INSERT INTO ${table} (${columns.join(
393
+ const safeQuery = `INSERT INTO ${safeTable} (${safeColumns.join(
310
394
  ", "
311
395
  )}) VALUES [--REDACTED--]];`;
312
396
  return new Promise((resolve) => {
@@ -325,21 +409,27 @@ function upsert(table, uuid, record, options) {
325
409
  const { connection } = state;
326
410
  try {
327
411
  const [recordData] = expandReferences(state, record);
412
+ const safeTable = escapeIdentifier(table);
328
413
  const columns = Object.keys(recordData).sort();
329
- const selectValues = columns.map((key) => `'${escape(recordData[key])}' AS ${key}`).join(", ");
330
- const updateValues = columns.map((key) => `[Target].${key}='${escape(recordData[key])}'`).join(", ");
331
- const insertColumns = columns.join(", ");
332
- const insertValues = columns.map((key) => `[Source].${key}`).join(", ");
414
+ const safeColumns = columns.map((col) => escapeIdentifier(col));
415
+ const selectValues = columns.map((key, i) => `'${escape(recordData[key])}' AS ${safeColumns[i]}`).join(", ");
416
+ const updateValues = columns.map(
417
+ (key, i) => `[Target].${safeColumns[i]}='${escape(recordData[key])}'`
418
+ ).join(", ");
419
+ const insertColumns = safeColumns.join(", ");
420
+ const insertValues = safeColumns.map((col) => `[Source].${col}`).join(", ");
333
421
  const constraint = [];
334
422
  if (Array.isArray(uuid)) {
335
423
  uuid.forEach((key) => {
336
- constraint.push(`[Target].${key} = [Source].${key}`);
424
+ const safeKey = escapeIdentifier(key);
425
+ constraint.push(`[Target].${safeKey} = [Source].${safeKey}`);
337
426
  });
338
427
  } else {
339
- constraint.push(`[Target].${uuid} = [Source].${uuid}`);
428
+ const safeUuid = escapeIdentifier(uuid);
429
+ constraint.push(`[Target].${safeUuid} = [Source].${safeUuid}`);
340
430
  }
341
431
  const query = handleValues(
342
- `MERGE ${table} AS [Target]
432
+ `MERGE ${safeTable} AS [Target]
343
433
  USING (SELECT ${selectValues}) AS [Source]
344
434
  ON ${constraint.join(" AND ")}
345
435
  WHEN MATCHED THEN
@@ -348,7 +438,7 @@ function upsert(table, uuid, record, options) {
348
438
  INSERT (${insertColumns}) VALUES (${insertValues});`,
349
439
  handleOptions(options)
350
440
  );
351
- const safeQuery = `MERGE ${table} AS [Target]
441
+ const safeQuery = `MERGE ${safeTable} AS [Target]
352
442
  USING (SELECT [--REDACTED--])
353
443
  ON [Target].[--VALUE--] = [Source].[--VALUE--]
354
444
  WHEN MATCHED THEN
@@ -428,23 +518,27 @@ function upsertMany(table, uuid, records, options) {
428
518
  console.log("No records provided; skipping upsert.");
429
519
  resolve(state);
430
520
  }
521
+ const safeTable = escapeIdentifier(table);
431
522
  const columns = Object.keys(recordData[0]);
523
+ const safeColumns = columns.map((col) => escapeIdentifier(col));
432
524
  const valueSets = recordData.map(
433
525
  (x) => `('${escape(Object.values(x)).join("', '")}')`
434
526
  );
435
- const insertColumns = columns.join(", ");
436
- const insertValues = columns.map((key) => `[Source].${key}`).join(", ");
437
- const updateValues = columns.map((key) => `[Target].${key}=[Source].${key}`).join(", ");
527
+ const insertColumns = safeColumns.join(", ");
528
+ const insertValues = safeColumns.map((col) => `[Source].${col}`).join(", ");
529
+ const updateValues = safeColumns.map((col) => `[Target].${col}=[Source].${col}`).join(", ");
438
530
  const constraint = [];
439
531
  if (Array.isArray(uuid)) {
440
532
  uuid.forEach((key) => {
441
- constraint.push(`[Target].${key} = [Source].${key}`);
533
+ const safeKey = escapeIdentifier(key);
534
+ constraint.push(`[Target].${safeKey} = [Source].${safeKey}`);
442
535
  });
443
536
  } else {
444
- constraint.push(`[Target].${uuid} = [Source].${uuid}`);
537
+ const safeUuid = escapeIdentifier(uuid);
538
+ constraint.push(`[Target].${safeUuid} = [Source].${safeUuid}`);
445
539
  }
446
540
  const query = handleValues(
447
- `MERGE ${table} AS [Target]
541
+ `MERGE ${safeTable} AS [Target]
448
542
  USING (VALUES ${valueSets.join(", ")}) AS [Source] (${insertColumns})
449
543
  ON ${constraint.join(" AND ")}
450
544
  WHEN MATCHED THEN
@@ -453,7 +547,7 @@ function upsertMany(table, uuid, records, options) {
453
547
  INSERT (${insertColumns}) VALUES (${insertValues});`,
454
548
  handleOptions(options)
455
549
  );
456
- const safeQuery = `MERGE ${table} AS [Target]
550
+ const safeQuery = `MERGE ${safeTable} AS [Target]
457
551
  USING (VALUES [--REDACTED--]) AS [SOURCE] (${insertColumns})
458
552
  ON [Target].[--VALUE--] = [Source].[--VALUE--]
459
553
  WHEN MATCHED THEN
@@ -498,10 +592,12 @@ function insertTable(tableName, columns, options) {
498
592
  resolve(state);
499
593
  return state;
500
594
  }
501
- const structureData = data.map(
502
- (x) => `${x.name} ${x.type} ${x.hasOwnProperty("default") ? x.type.includes("varchar") || x.type.includes("text") ? `DEFAULT '${x.default}'` : `DEFAULT ${x.default}` : ""} ${x.unique ? "UNIQUE" : ""} ${x.identity ? "PRIMARY KEY IDENTITY (1,1)" : ""} ${x.required ? "NOT NULL" : ""}`
503
- ).join(", ");
504
- const query = `CREATE TABLE ${tableName} (
595
+ const safeTableName = escapeIdentifier(tableName);
596
+ const structureData = data.map((x) => {
597
+ const safeColName = escapeIdentifier(x.name);
598
+ return `${safeColName} ${x.type} ${x.hasOwnProperty("default") ? x.type.includes("varchar") || x.type.includes("text") ? `DEFAULT '${escape(x.default)}'` : `DEFAULT ${x.default}` : ""} ${x.unique ? "UNIQUE" : ""} ${x.identity ? "PRIMARY KEY IDENTITY (1,1)" : ""} ${x.required ? "NOT NULL" : ""}`;
599
+ }).join(", ");
600
+ const query = `CREATE TABLE ${safeTableName} (
505
601
  ${structureData}
506
602
  );`;
507
603
  console.log("Preparing to create table via:", query);
@@ -524,10 +620,12 @@ function modifyTable(tableName, columns, options) {
524
620
  resolve(state);
525
621
  return state;
526
622
  }
527
- const structureData = data.map(
528
- (x) => `${x.name} ${x.type} ${x.hasOwnProperty("default") ? x.type.includes("varchar") || x.type.includes("text") ? `DEFAULT '${x.default}'` : `DEFAULT ${x.default}` : ""} ${x.unique ? "UNIQUE" : ""} ${x.identity ? "IDENTITY (1,1)" : ""} ${x.required ? "NOT NULL" : ""}`
529
- ).join(", ");
530
- const query = `ALTER TABLE ${tableName} ADD ${structureData};`;
623
+ const safeTableName = escapeIdentifier(tableName);
624
+ const structureData = data.map((x) => {
625
+ const safeColName = escapeIdentifier(x.name);
626
+ return `${safeColName} ${x.type} ${x.hasOwnProperty("default") ? x.type.includes("varchar") || x.type.includes("text") ? `DEFAULT '${escape(x.default)}'` : `DEFAULT ${x.default}` : ""} ${x.unique ? "UNIQUE" : ""} ${x.identity ? "IDENTITY (1,1)" : ""} ${x.required ? "NOT NULL" : ""}`;
627
+ }).join(", ");
628
+ const query = `ALTER TABLE ${safeTableName} ADD ${structureData};`;
531
629
  console.log("Preparing to modify table via:", query);
532
630
  resolve(queryHandler(state, query, flattenRows, options));
533
631
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@openfn/language-mssql",
3
3
  "label": "MSSQL",
4
- "version": "6.0.6",
4
+ "version": "7.0.0",
5
5
  "description": "A Microsoft SQL language pack for OpenFn",
6
6
  "exports": {
7
7
  ".": {
@@ -26,7 +26,7 @@
26
26
  ],
27
27
  "dependencies": {
28
28
  "tedious": "18.6.1",
29
- "@openfn/language-common": "3.2.0"
29
+ "@openfn/language-common": "3.2.1"
30
30
  },
31
31
  "devDependencies": {
32
32
  "assertion-error": "2.0.0",
@@ -12,15 +12,23 @@
12
12
  */
13
13
  export function execute(...operations: Operations): Operation;
14
14
  /**
15
- * Execute an SQL statement
15
+ * Execute a raw SQL statement
16
16
  * @public
17
17
  * @example
18
- * sql({ query, options })
18
+ * sql({
19
+ * query: 'SELECT * FROM users WHERE id = @id',
20
+ * options
21
+ * })
19
22
  * @function
20
- * @param {object} params - Payload data for the message
23
+ * @param {object} params - Payload data for the SQL query
24
+ * @param {string} params.query - The SQL query to execute
25
+ * @param {object} params.options - Optional query options
21
26
  * @returns {Operation}
22
27
  */
23
- export function sql(params: object): Operation;
28
+ export function sql(params: {
29
+ query: string;
30
+ options: object;
31
+ }): Operation;
24
32
  /**
25
33
  * Fetch a uuid key given a condition
26
34
  * @public
package/types/util.d.ts CHANGED
@@ -6,8 +6,49 @@
6
6
  * util.escape('string with "quotes"') // 'string with "quotes"'
7
7
  * util.escape('') // ''
8
8
  * util.escape(null) // null
9
+ * util.escape(123) // '123'
10
+ * util.escape(true) // 'true'
9
11
  * util.escape("'; DROP TABLE users; --") // "''; DROP TABLE users; --"
10
- * @param {string} stringExp - The string to escape.
11
- * @returns {string} The escaped string.
12
+ * @param {string|number|boolean|null|undefined} stringExp - The value to escape.
13
+ * @returns {string|null|undefined} The escaped string, or null/undefined if input was null/undefined.
14
+ * @throws {Error} If the value is an unexpected type (object, array, function, etc.).
12
15
  */
13
- export function escape(stringExp: string): string;
16
+ export function escape(stringExp: string | number | boolean | null | undefined): string | null | undefined;
17
+ /**
18
+ * Escapes a string for use in SQL LIKE patterns, preventing wildcard injection.
19
+ * This should be used when the operator is LIKE or NOT LIKE.
20
+ * @example <caption>Examples</caption>
21
+ * util.escapeLike('test') // 'test'
22
+ * util.escapeLike('100%') // '100[%]'
23
+ * util.escapeLike('test_value') // 'test[_]value'
24
+ * util.escapeLike('[admin]') // '[[[]admin]'
25
+ * util.escapeLike("O'Connor%") // "O''Connor[%]"
26
+ * @param {string} pattern - The pattern to escape.
27
+ * @returns {string} The escaped pattern safe for use in LIKE clauses.
28
+ */
29
+ export function escapeLike(pattern: string): string;
30
+ /**
31
+ * Escapes an SQL identifier (table or column name) to prevent SQL injection.
32
+ * Uses bracket notation for SQL Server which is safer than validating patterns.
33
+ * @example <caption>Examples</caption>
34
+ * util.escapeIdentifier('users') // '[users]'
35
+ * util.escapeIdentifier('first_name') // '[first_name]'
36
+ * util.escapeIdentifier('user]table') // '[user]]table]'
37
+ * util.escapeIdentifier('my.table') // '[my.table]'
38
+ * @param {string} identifier - The identifier to escape.
39
+ * @returns {string} The escaped identifier wrapped in brackets.
40
+ * @throws {Error} If the identifier is not a string or is empty.
41
+ */
42
+ export function escapeIdentifier(identifier: string): string;
43
+ /**
44
+ * Validates and normalizes SQL operators to prevent SQL injection.
45
+ * @example <caption>Examples</caption>
46
+ * util.validateOperator('=') // '='
47
+ * util.validateOperator('like') // 'LIKE'
48
+ * util.validateOperator('>=') // '>='
49
+ * util.validateOperator("' OR '1'='1") // throws Error
50
+ * @param {string} operator - The operator to validate.
51
+ * @returns {string} The validated and normalized operator.
52
+ * @throws {Error} If the operator is not in the allowed list.
53
+ */
54
+ export function validateOperator(operator: string): string;