@openfn/language-mysql 3.0.5 → 4.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
@@ -13,6 +13,11 @@
13
13
  "title": "example",
14
14
  "description": "sql(state => `select * from ${state.data.tableName};`, { writeSql: true })"
15
15
  },
16
+ {
17
+ "title": "example",
18
+ "description": "sql(state => `select * from ?? where id = ?;`, {\n values: state => [state.data.tableName, state.data.id],\n});",
19
+ "caption": "Prepared statements"
20
+ },
16
21
  {
17
22
  "title": "function",
18
23
  "description": null,
@@ -25,59 +30,28 @@
25
30
  },
26
31
  {
27
32
  "title": "param",
28
- "description": "The SQL query as a string or a function that returns a string using state.",
33
+ "description": "The sql query string.",
29
34
  "type": {
30
- "type": "UnionType",
31
- "elements": [
32
- {
33
- "type": "NameExpression",
34
- "name": "string"
35
- },
36
- {
37
- "type": "NameExpression",
38
- "name": "function"
39
- }
40
- ]
35
+ "type": "NameExpression",
36
+ "name": "string"
41
37
  },
42
38
  "name": "sqlQuery"
43
39
  },
44
40
  {
45
41
  "title": "param",
46
- "description": "Optional options argument.",
42
+ "description": " The sql query options.",
47
43
  "type": {
48
44
  "type": "OptionalType",
49
45
  "expression": {
50
46
  "type": "NameExpression",
51
- "name": "object"
47
+ "name": "sqlOptions"
52
48
  }
53
49
  },
54
50
  "name": "options"
55
51
  },
56
52
  {
57
- "title": "param",
58
- "description": "If true, logs the generated SQL statement. Defaults to false.",
59
- "type": {
60
- "type": "OptionalType",
61
- "expression": {
62
- "type": "NameExpression",
63
- "name": "boolean"
64
- }
65
- },
66
- "name": "options.writeSql",
67
- "default": "false"
68
- },
69
- {
70
- "title": "param",
71
- "description": "If false, does not execute the SQL, just logs it and adds to state.queries. Defaults to true.",
72
- "type": {
73
- "type": "OptionalType",
74
- "expression": {
75
- "type": "NameExpression",
76
- "name": "boolean"
77
- }
78
- },
79
- "name": "options.execute",
80
- "default": "true"
53
+ "title": "state",
54
+ "description": "{MySQLState}"
81
55
  },
82
56
  {
83
57
  "title": "returns",
@@ -89,7 +63,7 @@
89
63
  }
90
64
  ]
91
65
  },
92
- "valid": false
66
+ "valid": true
93
67
  },
94
68
  {
95
69
  "name": "insert",
@@ -102,8 +76,8 @@
102
76
  "tags": [
103
77
  {
104
78
  "title": "example",
105
- "description": "insert(\"users\", { name: (state) => state.data.name });",
106
- "caption": "Insert a record into the `users` table"
79
+ "description": "insert(\"users\", { name: \"one\", email: \"one@openfn.org\" });",
80
+ "caption": "Insert a record into a table"
107
81
  },
108
82
  {
109
83
  "title": "function",
@@ -133,6 +107,10 @@
133
107
  },
134
108
  "name": "fields"
135
109
  },
110
+ {
111
+ "title": "state",
112
+ "description": "{MySQLState}"
113
+ },
136
114
  {
137
115
  "title": "returns",
138
116
  "description": null,
@@ -156,8 +134,8 @@
156
134
  "tags": [
157
135
  {
158
136
  "title": "example",
159
- "description": "upsert(\"table\", { name: (state) => state.data.name });",
160
- "caption": "Upsert a record"
137
+ "description": "upsert(\"users\", { name: \"Tuchi Dev\" });",
138
+ "caption": "Upsert a record into a table"
161
139
  },
162
140
  {
163
141
  "title": "function",
@@ -187,6 +165,10 @@
187
165
  },
188
166
  "name": "fields"
189
167
  },
168
+ {
169
+ "title": "state",
170
+ "description": "{MySQLState}"
171
+ },
190
172
  {
191
173
  "title": "returns",
192
174
  "description": null,
@@ -215,8 +197,8 @@
215
197
  },
216
198
  {
217
199
  "title": "example",
218
- "description": "upsertMany(\n 'users', // the DB table\n [\n { name: 'one', email: 'one@openfn.org' },\n { name: 'two', email: 'two@openfn.org' },\n ]\n)",
219
- "caption": "Upsert multiple records"
200
+ "description": "upsertMany(\n \"users\", // the DB table\n [\n { name: \"one\", email: \"one@openfn.org\" },\n { name: \"two\", email: \"two@openfn.org\" },\n ]\n);",
201
+ "caption": "Upsert multiple records into a table"
220
202
  },
221
203
  {
222
204
  "title": "function",
@@ -239,13 +221,17 @@
239
221
  },
240
222
  {
241
223
  "title": "param",
242
- "description": "An array of objects or a function that returns an array",
224
+ "description": "An array of objects fields",
243
225
  "type": {
244
226
  "type": "NameExpression",
245
227
  "name": "array"
246
228
  },
247
229
  "name": "data"
248
230
  },
231
+ {
232
+ "title": "state",
233
+ "description": "{MySQLState}"
234
+ },
249
235
  {
250
236
  "title": "returns",
251
237
  "description": null,
@@ -929,6 +915,59 @@
929
915
  },
930
916
  "valid": false
931
917
  },
918
+ {
919
+ "name": "assert",
920
+ "params": [
921
+ "expression",
922
+ "errorMessage"
923
+ ],
924
+ "docs": {
925
+ "description": "Asserts the given expression or function resolves to `true`, or else throws an exception. Optionally accepts and error message.",
926
+ "tags": [
927
+ {
928
+ "title": "public",
929
+ "description": null,
930
+ "type": null
931
+ },
932
+ {
933
+ "title": "function",
934
+ "description": null,
935
+ "name": null
936
+ },
937
+ {
938
+ "title": "example",
939
+ "description": "assert('a' === 'b', '\"a\" is not equal to \"b\"')"
940
+ },
941
+ {
942
+ "title": "param",
943
+ "description": "The expression or function to be evaluated.",
944
+ "type": {
945
+ "type": "NameExpression",
946
+ "name": "any"
947
+ },
948
+ "name": "expression"
949
+ },
950
+ {
951
+ "title": "param",
952
+ "description": "The error message thrown in case of a failed state.",
953
+ "type": {
954
+ "type": "NameExpression",
955
+ "name": "string"
956
+ },
957
+ "name": "errorMessage"
958
+ },
959
+ {
960
+ "title": "returns",
961
+ "description": null,
962
+ "type": {
963
+ "type": "NameExpression",
964
+ "name": "operation"
965
+ }
966
+ }
967
+ ]
968
+ },
969
+ "valid": true
970
+ },
932
971
  {
933
972
  "name": "as",
934
973
  "params": [
package/dist/index.cjs CHANGED
@@ -28,6 +28,7 @@ __export(src_exports, {
28
28
  alterState: () => import_language_common2.alterState,
29
29
  arrayToString: () => import_language_common2.arrayToString,
30
30
  as: () => import_language_common2.as,
31
+ assert: () => import_language_common2.assert,
31
32
  combine: () => import_language_common2.combine,
32
33
  cursor: () => import_language_common2.cursor,
33
34
  dataPath: () => import_language_common2.dataPath,
@@ -43,6 +44,7 @@ __export(src_exports, {
43
44
  insert: () => insert,
44
45
  lastReferenceValue: () => import_language_common2.lastReferenceValue,
45
46
  merge: () => import_language_common2.merge,
47
+ setMockConnection: () => setMockConnection,
46
48
  sourceValue: () => import_language_common2.sourceValue,
47
49
  sql: () => sql,
48
50
  upsert: () => upsert,
@@ -56,6 +58,7 @@ __export(Adaptor_exports, {
56
58
  alterState: () => import_language_common2.alterState,
57
59
  arrayToString: () => import_language_common2.arrayToString,
58
60
  as: () => import_language_common2.as,
61
+ assert: () => import_language_common2.assert,
59
62
  combine: () => import_language_common2.combine,
60
63
  cursor: () => import_language_common2.cursor,
61
64
  dataPath: () => import_language_common2.dataPath,
@@ -70,6 +73,7 @@ __export(Adaptor_exports, {
70
73
  insert: () => insert,
71
74
  lastReferenceValue: () => import_language_common2.lastReferenceValue,
72
75
  merge: () => import_language_common2.merge,
76
+ setMockConnection: () => setMockConnection,
73
77
  sourceValue: () => import_language_common2.sourceValue,
74
78
  sql: () => sql,
75
79
  upsert: () => upsert,
@@ -77,175 +81,187 @@ __export(Adaptor_exports, {
77
81
  });
78
82
  var import_language_common = require("@openfn/language-common");
79
83
  var import_util = require("@openfn/language-common/util");
80
- var import_mysql = __toESM(require("mysql"), 1);
81
- var import_squel = __toESM(require("squel"), 1);
84
+ var import_promise = __toESM(require("mysql2/promise"), 1);
85
+ var import_knex = __toESM(require("knex"), 1);
82
86
  var import_language_common2 = require("@openfn/language-common");
83
- function sql(sqlQuery, options = {}) {
87
+ var connection;
88
+ var knexInstance = (0, import_knex.default)({ client: "mysql2" });
89
+ async function connect(state) {
90
+ if (connection) {
91
+ return state;
92
+ }
93
+ const { host, port, database, password, user } = state.configuration || {};
94
+ try {
95
+ connection = await import_promise.default.createConnection({
96
+ host,
97
+ user,
98
+ password,
99
+ database,
100
+ port
101
+ });
102
+ console.log("Connected to database...");
103
+ } catch (error) {
104
+ console.log(error);
105
+ throw new Error("Unable to connect to database.");
106
+ }
107
+ return state;
108
+ }
109
+ async function disconnect(state) {
110
+ if (connection) {
111
+ await connection.end();
112
+ console.log("Disconected from database...");
113
+ connection = null;
114
+ }
115
+ knexInstance.destroy();
116
+ return state;
117
+ }
118
+ function formatFields(fields2) {
119
+ if (!fields2)
120
+ return void 0;
121
+ return fields2.map((field2) => ({
122
+ name: field2.name,
123
+ type: field2.type,
124
+ characterSet: field2.characterSet,
125
+ table: field2.orgTable,
126
+ schema: field2.schema,
127
+ length: field2.columnLength,
128
+ decimals: field2.decimals,
129
+ flags: field2.flags,
130
+ encoding: field2.encoding
131
+ }));
132
+ }
133
+ function setMockConnection(mockConnection) {
134
+ connection = mockConnection;
135
+ }
136
+ function execute(...operations) {
137
+ const initialState = {
138
+ queries: []
139
+ };
84
140
  return (state) => {
85
- const { connection } = state;
141
+ return (0, import_language_common.execute)(
142
+ connect,
143
+ ...operations,
144
+ disconnect
145
+ )({ ...initialState, ...state });
146
+ };
147
+ }
148
+ function sql(sqlQuery, options = {}) {
149
+ return async (state) => {
86
150
  const [resolvedSqlQuery, resolvedOptions] = (0, import_util.expandReferences)(
87
151
  state,
88
152
  sqlQuery,
89
153
  options
90
154
  );
91
- const { writeSql = false, execute: execute2 = true } = resolvedOptions;
155
+ const { writeSql = false, execute: execute2 = true, values } = resolvedOptions;
92
156
  if (writeSql) {
93
157
  console.log("Prepared SQL:", resolvedSqlQuery);
158
+ if (values) {
159
+ state.queries.push({ sql: resolvedSqlQuery, values });
160
+ } else {
161
+ state.queries.push(resolvedSqlQuery);
162
+ }
94
163
  }
95
164
  if (!execute2) {
165
+ console.log("Execution skipped, execute is false.");
96
166
  return {
97
167
  ...state,
98
- queries: [...state.queries || [], resolvedSqlQuery]
168
+ queries: [...state.queries, resolvedSqlQuery]
99
169
  };
100
170
  }
101
- return new Promise((resolve, reject) => {
102
- connection.query(resolvedSqlQuery, (err, results, fields2) => {
103
- if (err) {
104
- console.log("Error executing query. Disconnecting from database.");
105
- connection.end();
106
- return reject(err);
107
- }
108
- resolve({ ...state, response: { body: results, fields: fields2 } });
171
+ try {
172
+ const [result, fields2] = await connection.execute(
173
+ resolvedSqlQuery,
174
+ values
175
+ );
176
+ console.log("Query executed successfully.");
177
+ return (0, import_language_common.composeNextState)(state, {
178
+ result,
179
+ fields: formatFields(fields2)
109
180
  });
110
- });
111
- };
112
- }
113
- function execute(...operations) {
114
- const initialState = {
115
- references: [],
116
- data: null
117
- };
118
- return (state) => {
119
- return (0, import_language_common.execute)(
120
- connect,
121
- ...operations,
122
- disconnect,
123
- cleanupState
124
- )({ ...initialState, ...state });
181
+ } catch (err) {
182
+ console.log("Error executing query.");
183
+ throw err;
184
+ }
125
185
  };
126
186
  }
127
- function connect(state) {
128
- const { host, port, database, password, user } = state.configuration;
129
- var connection = import_mysql.default.createConnection({
130
- host,
131
- user,
132
- password,
133
- database,
134
- port
135
- });
136
- connection.connect();
137
- console.log(`Preparing to query "` + database + `"...`);
138
- return { ...state, connection };
139
- }
140
- function disconnect(state) {
141
- state.connection.end();
142
- return state;
143
- }
144
- function cleanupState(state) {
145
- delete state.connection;
146
- return state;
147
- }
148
187
  function insert(table, fields2) {
149
- return (state) => {
150
- let { connection } = state;
151
- const [valuesObj] = (0, import_util.expandReferences)(state, fields2);
152
- const squelMysql = import_squel.default.useFlavour("mysql");
153
- var sqlParams = squelMysql.insert({
154
- autoQuoteFieldNames: true
155
- }).into(table).setFields(valuesObj).toParam();
156
- const sql2 = sqlParams.text;
157
- const inserts = sqlParams.values;
158
- const sqlString = import_mysql.default.format(sql2, inserts);
159
- console.log(`Executing MySQL query: ${sqlString}`);
160
- return new Promise((resolve, reject) => {
161
- connection.query(sqlString, function(err, results, fields3) {
162
- if (err) {
163
- reject(err);
164
- console.log("There is an error. Disconnecting from database.");
165
- connection.end();
166
- } else {
167
- console.log("Success...");
168
- console.log(results);
169
- console.log(fields3);
170
- resolve(results);
171
- }
188
+ return async (state) => {
189
+ const [resolvedTable, resolvedFields] = (0, import_util.expandReferences)(
190
+ state,
191
+ table,
192
+ fields2
193
+ );
194
+ const keys = Object.keys(resolvedFields);
195
+ const placeholders = keys.map(() => "?").join(", ");
196
+ const columns = keys.map(() => "??").join(", ");
197
+ const sqlString = import_promise.default.format(
198
+ `INSERT INTO ?? (${columns}) VALUES (${placeholders})`,
199
+ [resolvedTable, ...keys]
200
+ );
201
+ try {
202
+ const [result, fields3] = await connection.execute(
203
+ sqlString,
204
+ Object.values(resolvedFields)
205
+ );
206
+ console.log("Success...");
207
+ return (0, import_language_common.composeNextState)(state, {
208
+ result,
209
+ fields: formatFields(fields3)
172
210
  });
173
- }).then((data) => {
174
- const nextState = { ...state, response: { body: data } };
175
- return nextState;
176
- });
211
+ } catch (err) {
212
+ console.log("Error inserting record.");
213
+ throw err;
214
+ }
177
215
  };
178
216
  }
179
217
  function upsert(table, fields2) {
180
- return (state) => {
181
- let { connection } = state;
182
- const [valuesObj] = (0, import_util.expandReferences)(state, fields2);
183
- const squelMysql = import_squel.default.useFlavour("mysql");
184
- var insertParams = squelMysql.insert({
185
- autoQuoteFieldNames: true
186
- }).into(table).setFields(valuesObj).toParam();
187
- var sql2 = insertParams.text;
188
- var inserts = insertParams.values;
189
- const insertString = import_mysql.default.format(sql2, inserts);
190
- var updateParams = squelMysql.update({
191
- autoQuoteFieldNames: true
192
- }).table("").setFields(valuesObj).toParam();
193
- var sql2 = updateParams.text;
194
- var inserts = updateParams.values;
195
- const updateString = import_mysql.default.format(sql2, inserts);
196
- const upsertString = insertString + ` ON DUPLICATE KEY UPDATE ` + updateString.slice(10);
218
+ return async (state) => {
219
+ const [resolvedTable, resolvedFields] = (0, import_util.expandReferences)(
220
+ state,
221
+ table,
222
+ fields2
223
+ );
224
+ const insertQuery = knexInstance(resolvedTable).insert(resolvedFields);
225
+ const insertString = insertQuery.toString();
226
+ const updateParts = Object.keys(resolvedFields).map(
227
+ (key) => `${knexInstance.ref(key)} = VALUES(${knexInstance.ref(key)})`
228
+ );
229
+ const updateClause = updateParts.join(", ");
230
+ const upsertString = `${insertString} ON DUPLICATE KEY UPDATE ${updateClause}`;
197
231
  console.log("Executing MySQL query: " + upsertString);
198
- return new Promise((resolve, reject) => {
199
- connection.query(upsertString, function(err, results, fields3) {
200
- if (err) {
201
- reject(err);
202
- console.log("That's an error. Disconnecting from database.");
203
- connection.end();
204
- } else {
205
- console.log("Success...");
206
- console.log(results);
207
- console.log(fields3);
208
- resolve(results);
209
- }
210
- });
211
- }).then((data) => {
212
- const nextState = { ...state, response: { body: data } };
213
- return nextState;
214
- });
232
+ try {
233
+ const [result, fields3] = await connection.execute(upsertString);
234
+ console.log("Success...");
235
+ return (0, import_language_common.composeNextState)(state, { result, fields: formatFields(fields3) });
236
+ } catch (err) {
237
+ console.log("That's an error.");
238
+ throw err;
239
+ }
215
240
  };
216
241
  }
217
242
  function upsertMany(table, data) {
218
- return function(state) {
219
- return new Promise(function(resolve, reject) {
220
- const [rows] = (0, import_util.expandReferences)(state, data);
221
- if (!rows || rows.length === 0) {
222
- console.log("No records provided; skipping upsert.");
223
- resolve(state);
224
- }
225
- const squelMysql = import_squel.default.useFlavour("mysql");
226
- const columns = Object.keys(rows[0]);
227
- let upsertSql = squelMysql.insert().into(table).setFieldsRows(rows);
228
- columns.map((c) => {
229
- upsertSql = upsertSql.onDupUpdate(`${c}=values(${c})`);
230
- });
231
- const upsertString = upsertSql.toString();
232
- let { connection } = state;
233
- connection.query(upsertString, function(err, results, fields2) {
234
- if (err) {
235
- reject(err);
236
- console.log("That's an error. Disconnecting from database.");
237
- connection.end();
238
- } else {
239
- console.log("Success...");
240
- console.log(results);
241
- console.log(fields2);
242
- resolve(results);
243
- }
244
- });
245
- }).then(function(data2) {
246
- const nextState = { ...state, response: { body: data2 } };
247
- return nextState;
248
- });
243
+ return async (state) => {
244
+ const [resolvedTable, resolvedData] = (0, import_util.expandReferences)(state, table, data);
245
+ if (!resolvedData || resolvedData.length === 0) {
246
+ console.log("No records provided; skipping upsert.");
247
+ return state;
248
+ }
249
+ const columns = Object.keys(resolvedData[0]);
250
+ const insertQuery = knexInstance(resolvedTable).insert(resolvedData);
251
+ const insertString = insertQuery.toString();
252
+ const updateParts = columns.map(
253
+ (c) => `${knexInstance.ref(c)} = VALUES(${knexInstance.ref(c)})`
254
+ );
255
+ const updateClause = updateParts.join(", ");
256
+ const upsertString = `${insertString} ON DUPLICATE KEY UPDATE ${updateClause}`;
257
+ try {
258
+ const [result, fields2] = await connection.execute(upsertString);
259
+ console.log("Success...");
260
+ return (0, import_language_common.composeNextState)(state, { result, fields: formatFields(fields2) });
261
+ } catch (err) {
262
+ console.log("That's an error.");
263
+ throw err;
264
+ }
249
265
  };
250
266
  }
251
267
 
@@ -256,6 +272,7 @@ var src_default = Adaptor_exports;
256
272
  alterState,
257
273
  arrayToString,
258
274
  as,
275
+ assert,
259
276
  combine,
260
277
  cursor,
261
278
  dataPath,
@@ -270,6 +287,7 @@ var src_default = Adaptor_exports;
270
287
  insert,
271
288
  lastReferenceValue,
272
289
  merge,
290
+ setMockConnection,
273
291
  sourceValue,
274
292
  sql,
275
293
  upsert,
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ __export(Adaptor_exports, {
10
10
  alterState: () => alterState,
11
11
  arrayToString: () => arrayToString,
12
12
  as: () => as,
13
+ assert: () => assert,
13
14
  combine: () => combine,
14
15
  cursor: () => cursor,
15
16
  dataPath: () => dataPath,
@@ -24,15 +25,19 @@ __export(Adaptor_exports, {
24
25
  insert: () => insert,
25
26
  lastReferenceValue: () => lastReferenceValue,
26
27
  merge: () => merge,
28
+ setMockConnection: () => setMockConnection,
27
29
  sourceValue: () => sourceValue,
28
30
  sql: () => sql,
29
31
  upsert: () => upsert,
30
32
  upsertMany: () => upsertMany
31
33
  });
32
- import { execute as commonExecute } from "@openfn/language-common";
34
+ import {
35
+ execute as commonExecute,
36
+ composeNextState
37
+ } from "@openfn/language-common";
33
38
  import { expandReferences } from "@openfn/language-common/util";
34
- import mysql from "mysql";
35
- import squel from "squel";
39
+ import mysql from "mysql2/promise";
40
+ import knex from "knex";
36
41
  import {
37
42
  fn,
38
43
  fnIf,
@@ -40,6 +45,7 @@ import {
40
45
  merge,
41
46
  field,
42
47
  fields,
48
+ assert,
43
49
  cursor,
44
50
  dateFns,
45
51
  combine,
@@ -51,172 +57,184 @@ import {
51
57
  lastReferenceValue,
52
58
  as
53
59
  } from "@openfn/language-common";
54
- function sql(sqlQuery, options = {}) {
60
+ var connection;
61
+ var knexInstance = knex({ client: "mysql2" });
62
+ async function connect(state) {
63
+ if (connection) {
64
+ return state;
65
+ }
66
+ const { host, port, database, password, user } = state.configuration || {};
67
+ try {
68
+ connection = await mysql.createConnection({
69
+ host,
70
+ user,
71
+ password,
72
+ database,
73
+ port
74
+ });
75
+ console.log("Connected to database...");
76
+ } catch (error) {
77
+ console.log(error);
78
+ throw new Error("Unable to connect to database.");
79
+ }
80
+ return state;
81
+ }
82
+ async function disconnect(state) {
83
+ if (connection) {
84
+ await connection.end();
85
+ console.log("Disconected from database...");
86
+ connection = null;
87
+ }
88
+ knexInstance.destroy();
89
+ return state;
90
+ }
91
+ function formatFields(fields2) {
92
+ if (!fields2)
93
+ return void 0;
94
+ return fields2.map((field2) => ({
95
+ name: field2.name,
96
+ type: field2.type,
97
+ characterSet: field2.characterSet,
98
+ table: field2.orgTable,
99
+ schema: field2.schema,
100
+ length: field2.columnLength,
101
+ decimals: field2.decimals,
102
+ flags: field2.flags,
103
+ encoding: field2.encoding
104
+ }));
105
+ }
106
+ function setMockConnection(mockConnection) {
107
+ connection = mockConnection;
108
+ }
109
+ function execute(...operations) {
110
+ const initialState = {
111
+ queries: []
112
+ };
55
113
  return (state) => {
56
- const { connection } = state;
114
+ return commonExecute(
115
+ connect,
116
+ ...operations,
117
+ disconnect
118
+ )({ ...initialState, ...state });
119
+ };
120
+ }
121
+ function sql(sqlQuery, options = {}) {
122
+ return async (state) => {
57
123
  const [resolvedSqlQuery, resolvedOptions] = expandReferences(
58
124
  state,
59
125
  sqlQuery,
60
126
  options
61
127
  );
62
- const { writeSql = false, execute: execute2 = true } = resolvedOptions;
128
+ const { writeSql = false, execute: execute2 = true, values } = resolvedOptions;
63
129
  if (writeSql) {
64
130
  console.log("Prepared SQL:", resolvedSqlQuery);
131
+ if (values) {
132
+ state.queries.push({ sql: resolvedSqlQuery, values });
133
+ } else {
134
+ state.queries.push(resolvedSqlQuery);
135
+ }
65
136
  }
66
137
  if (!execute2) {
138
+ console.log("Execution skipped, execute is false.");
67
139
  return {
68
140
  ...state,
69
- queries: [...state.queries || [], resolvedSqlQuery]
141
+ queries: [...state.queries, resolvedSqlQuery]
70
142
  };
71
143
  }
72
- return new Promise((resolve, reject) => {
73
- connection.query(resolvedSqlQuery, (err, results, fields2) => {
74
- if (err) {
75
- console.log("Error executing query. Disconnecting from database.");
76
- connection.end();
77
- return reject(err);
78
- }
79
- resolve({ ...state, response: { body: results, fields: fields2 } });
144
+ try {
145
+ const [result, fields2] = await connection.execute(
146
+ resolvedSqlQuery,
147
+ values
148
+ );
149
+ console.log("Query executed successfully.");
150
+ return composeNextState(state, {
151
+ result,
152
+ fields: formatFields(fields2)
80
153
  });
81
- });
82
- };
83
- }
84
- function execute(...operations) {
85
- const initialState = {
86
- references: [],
87
- data: null
88
- };
89
- return (state) => {
90
- return commonExecute(
91
- connect,
92
- ...operations,
93
- disconnect,
94
- cleanupState
95
- )({ ...initialState, ...state });
154
+ } catch (err) {
155
+ console.log("Error executing query.");
156
+ throw err;
157
+ }
96
158
  };
97
159
  }
98
- function connect(state) {
99
- const { host, port, database, password, user } = state.configuration;
100
- var connection = mysql.createConnection({
101
- host,
102
- user,
103
- password,
104
- database,
105
- port
106
- });
107
- connection.connect();
108
- console.log(`Preparing to query "` + database + `"...`);
109
- return { ...state, connection };
110
- }
111
- function disconnect(state) {
112
- state.connection.end();
113
- return state;
114
- }
115
- function cleanupState(state) {
116
- delete state.connection;
117
- return state;
118
- }
119
160
  function insert(table, fields2) {
120
- return (state) => {
121
- let { connection } = state;
122
- const [valuesObj] = expandReferences(state, fields2);
123
- const squelMysql = squel.useFlavour("mysql");
124
- var sqlParams = squelMysql.insert({
125
- autoQuoteFieldNames: true
126
- }).into(table).setFields(valuesObj).toParam();
127
- const sql2 = sqlParams.text;
128
- const inserts = sqlParams.values;
129
- const sqlString = mysql.format(sql2, inserts);
130
- console.log(`Executing MySQL query: ${sqlString}`);
131
- return new Promise((resolve, reject) => {
132
- connection.query(sqlString, function(err, results, fields3) {
133
- if (err) {
134
- reject(err);
135
- console.log("There is an error. Disconnecting from database.");
136
- connection.end();
137
- } else {
138
- console.log("Success...");
139
- console.log(results);
140
- console.log(fields3);
141
- resolve(results);
142
- }
161
+ return async (state) => {
162
+ const [resolvedTable, resolvedFields] = expandReferences(
163
+ state,
164
+ table,
165
+ fields2
166
+ );
167
+ const keys = Object.keys(resolvedFields);
168
+ const placeholders = keys.map(() => "?").join(", ");
169
+ const columns = keys.map(() => "??").join(", ");
170
+ const sqlString = mysql.format(
171
+ `INSERT INTO ?? (${columns}) VALUES (${placeholders})`,
172
+ [resolvedTable, ...keys]
173
+ );
174
+ try {
175
+ const [result, fields3] = await connection.execute(
176
+ sqlString,
177
+ Object.values(resolvedFields)
178
+ );
179
+ console.log("Success...");
180
+ return composeNextState(state, {
181
+ result,
182
+ fields: formatFields(fields3)
143
183
  });
144
- }).then((data) => {
145
- const nextState = { ...state, response: { body: data } };
146
- return nextState;
147
- });
184
+ } catch (err) {
185
+ console.log("Error inserting record.");
186
+ throw err;
187
+ }
148
188
  };
149
189
  }
150
190
  function upsert(table, fields2) {
151
- return (state) => {
152
- let { connection } = state;
153
- const [valuesObj] = expandReferences(state, fields2);
154
- const squelMysql = squel.useFlavour("mysql");
155
- var insertParams = squelMysql.insert({
156
- autoQuoteFieldNames: true
157
- }).into(table).setFields(valuesObj).toParam();
158
- var sql2 = insertParams.text;
159
- var inserts = insertParams.values;
160
- const insertString = mysql.format(sql2, inserts);
161
- var updateParams = squelMysql.update({
162
- autoQuoteFieldNames: true
163
- }).table("").setFields(valuesObj).toParam();
164
- var sql2 = updateParams.text;
165
- var inserts = updateParams.values;
166
- const updateString = mysql.format(sql2, inserts);
167
- const upsertString = insertString + ` ON DUPLICATE KEY UPDATE ` + updateString.slice(10);
191
+ return async (state) => {
192
+ const [resolvedTable, resolvedFields] = expandReferences(
193
+ state,
194
+ table,
195
+ fields2
196
+ );
197
+ const insertQuery = knexInstance(resolvedTable).insert(resolvedFields);
198
+ const insertString = insertQuery.toString();
199
+ const updateParts = Object.keys(resolvedFields).map(
200
+ (key) => `${knexInstance.ref(key)} = VALUES(${knexInstance.ref(key)})`
201
+ );
202
+ const updateClause = updateParts.join(", ");
203
+ const upsertString = `${insertString} ON DUPLICATE KEY UPDATE ${updateClause}`;
168
204
  console.log("Executing MySQL query: " + upsertString);
169
- return new Promise((resolve, reject) => {
170
- connection.query(upsertString, function(err, results, fields3) {
171
- if (err) {
172
- reject(err);
173
- console.log("That's an error. Disconnecting from database.");
174
- connection.end();
175
- } else {
176
- console.log("Success...");
177
- console.log(results);
178
- console.log(fields3);
179
- resolve(results);
180
- }
181
- });
182
- }).then((data) => {
183
- const nextState = { ...state, response: { body: data } };
184
- return nextState;
185
- });
205
+ try {
206
+ const [result, fields3] = await connection.execute(upsertString);
207
+ console.log("Success...");
208
+ return composeNextState(state, { result, fields: formatFields(fields3) });
209
+ } catch (err) {
210
+ console.log("That's an error.");
211
+ throw err;
212
+ }
186
213
  };
187
214
  }
188
215
  function upsertMany(table, data) {
189
- return function(state) {
190
- return new Promise(function(resolve, reject) {
191
- const [rows] = expandReferences(state, data);
192
- if (!rows || rows.length === 0) {
193
- console.log("No records provided; skipping upsert.");
194
- resolve(state);
195
- }
196
- const squelMysql = squel.useFlavour("mysql");
197
- const columns = Object.keys(rows[0]);
198
- let upsertSql = squelMysql.insert().into(table).setFieldsRows(rows);
199
- columns.map((c) => {
200
- upsertSql = upsertSql.onDupUpdate(`${c}=values(${c})`);
201
- });
202
- const upsertString = upsertSql.toString();
203
- let { connection } = state;
204
- connection.query(upsertString, function(err, results, fields2) {
205
- if (err) {
206
- reject(err);
207
- console.log("That's an error. Disconnecting from database.");
208
- connection.end();
209
- } else {
210
- console.log("Success...");
211
- console.log(results);
212
- console.log(fields2);
213
- resolve(results);
214
- }
215
- });
216
- }).then(function(data2) {
217
- const nextState = { ...state, response: { body: data2 } };
218
- return nextState;
219
- });
216
+ return async (state) => {
217
+ const [resolvedTable, resolvedData] = expandReferences(state, table, data);
218
+ if (!resolvedData || resolvedData.length === 0) {
219
+ console.log("No records provided; skipping upsert.");
220
+ return state;
221
+ }
222
+ const columns = Object.keys(resolvedData[0]);
223
+ const insertQuery = knexInstance(resolvedTable).insert(resolvedData);
224
+ const insertString = insertQuery.toString();
225
+ const updateParts = columns.map(
226
+ (c) => `${knexInstance.ref(c)} = VALUES(${knexInstance.ref(c)})`
227
+ );
228
+ const updateClause = updateParts.join(", ");
229
+ const upsertString = `${insertString} ON DUPLICATE KEY UPDATE ${updateClause}`;
230
+ try {
231
+ const [result, fields2] = await connection.execute(upsertString);
232
+ console.log("Success...");
233
+ return composeNextState(state, { result, fields: formatFields(fields2) });
234
+ } catch (err) {
235
+ console.log("That's an error.");
236
+ throw err;
237
+ }
220
238
  };
221
239
  }
222
240
 
@@ -226,6 +244,7 @@ export {
226
244
  alterState,
227
245
  arrayToString,
228
246
  as,
247
+ assert,
229
248
  combine,
230
249
  cursor,
231
250
  dataPath,
@@ -241,6 +260,7 @@ export {
241
260
  insert,
242
261
  lastReferenceValue,
243
262
  merge,
263
+ setMockConnection,
244
264
  sourceValue,
245
265
  sql,
246
266
  upsert,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@openfn/language-mysql",
3
3
  "label": "MySQL",
4
- "version": "3.0.5",
4
+ "version": "4.0.0",
5
5
  "description": "A MySQL Language Pack for OpenFn",
6
6
  "homepage": "https://docs.openfn.org",
7
7
  "main": "dist/index.cjs",
@@ -18,11 +18,9 @@
18
18
  "configuration-schema.json"
19
19
  ],
20
20
  "dependencies": {
21
- "json-sql": "^0.3.10",
22
- "mysql": "^2.13.0",
23
- "squel": "^5.8.0",
24
- "string-escape": "^0.3.0",
25
- "@openfn/language-common": "3.2.0"
21
+ "knex": "^3.1.0",
22
+ "mysql2": "^3.15.3",
23
+ "@openfn/language-common": "3.2.1"
26
24
  },
27
25
  "devDependencies": {
28
26
  "assertion-error": "^1.0.1",
@@ -42,8 +40,9 @@
42
40
  "types": "types/index.d.ts",
43
41
  "scripts": {
44
42
  "build": "pnpm clean && build-adaptor mysql",
45
- "test": "mocha --experimental-specifier-resolution=node --no-warnings",
46
- "test:watch": "mocha -w --experimental-specifier-resolution=node --no-warnings",
43
+ "test": "mocha --experimental-specifier-resolution=node --no-warnings --exclude test/integration.js --recursive",
44
+ "test:watch": "mocha -w --experimental-specifier-resolution=node --no-warnings --exclude test/integration.js --recursive",
45
+ "test:integration": "mocha --experimental-specifier-resolution=node --no-warnings test/integration.js",
47
46
  "clean": "rimraf dist types docs",
48
47
  "pack": "pnpm pack --pack-destination ../../dist",
49
48
  "lint": "eslint src"
@@ -1,67 +1,126 @@
1
+ export function setMockConnection(mockConnection: any): void;
2
+ /**
3
+ * State object
4
+ * @typedef {Object} MySQLState
5
+ * @property data - the query results object
6
+ * @property data.result - the query result rows
7
+ * @property data.fields - the query result fields
8
+ * @property queries - an array of queries executed. Queries are added if `options.writeSql` is true.
9
+ * @property references - an array of all previous data objects used in the Job
10
+ * @private
11
+ **/
12
+ /**
13
+ * @typedef {Object} sqlOptions
14
+ * @property {array} [values] - An array of values for prepared statements.
15
+ * @property {boolean} [writeSql = false] - If true, logs the generated SQL statement. Defaults to false.
16
+ * @property {boolean} [execute = true] - If false, does not execute the SQL, just logs it and adds to state.queries. Defaults to true.
17
+ * */
18
+ /**
19
+ * Execute a sequence of operations.
20
+ * Wraps `language-common/execute`, and prepends initial state for mysql.
21
+ * @private
22
+ * @param {Operations} operations - Operations to be performed.
23
+ * @returns {Operation}
24
+ */
25
+ export function execute(...operations: Operations): Operation;
1
26
  /**
2
27
  * Execute a SQL statement. Take care when inserting values from state directly into a query,
3
28
  * as this can be a vector for injection attacks. See [OWASP SQL Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)
4
29
  * for guidelines
5
30
  * @example
6
31
  * sql(state => `select * from ${state.data.tableName};`, { writeSql: true })
32
+ * @example <caption>Prepared statements</caption>
33
+ * sql(state => `select * from ?? where id = ?;`, {
34
+ * values: state => [state.data.tableName, state.data.id],
35
+ * });
7
36
  * @function
8
37
  * @public
9
- * @param {string|function} sqlQuery - The SQL query as a string or a function that returns a string using state.
10
- * @param {object} [options] - Optional options argument.
11
- * @param {boolean} [options.writeSql = false] - If true, logs the generated SQL statement. Defaults to false.
12
- * @param {boolean} [options.execute = true] - If false, does not execute the SQL, just logs it and adds to state.queries. Defaults to true.
38
+ * @param {string} sqlQuery - The sql query string.
39
+ * @param {sqlOptions} [options] - The sql query options.
40
+ * @state {MySQLState}
13
41
  * @returns {Operation}
14
42
  */
15
- export function sql(sqlQuery: string | Function, options?: {
16
- writeSql?: boolean;
17
- execute?: boolean;
18
- }): Operation;
19
- /**
20
- * Execute a sequence of operations.
21
- * Wraps `language-common/execute`, and prepends initial state for mysql.
22
- * @private
23
- * @param {Operations} operations - Operations to be performed.
24
- * @returns {Operation}
25
- */
26
- export function execute(...operations: Operations): Operation;
43
+ export function sql(sqlQuery: string, options?: sqlOptions): Operation;
27
44
  /**
28
45
  * Insert a record
29
- * @example <caption>Insert a record into the `users` table</caption>
30
- * insert("users", { name: (state) => state.data.name });
46
+ * @example <caption>Insert a record into a table</caption>
47
+ * insert("users", { name: "one", email: "one@openfn.org" });
31
48
  * @function
32
49
  * @public
33
50
  * @param {string} table - The target table
34
51
  * @param {object} fields - A fields object
52
+ * @state {MySQLState}
35
53
  * @returns {Operation}
36
54
  */
37
55
  export function insert(table: string, fields: object): Operation;
38
56
  /**
39
57
  * Insert or Update a record if matched
40
- * @example <caption>Upsert a record</caption>
41
- * upsert("table", { name: (state) => state.data.name });
58
+ * @example <caption>Upsert a record into a table</caption>
59
+ * upsert("users", { name: "Tuchi Dev" });
42
60
  * @function
43
61
  * @public
44
62
  * @param {string} table - The target table
45
63
  * @param {object} fields - A fields object
64
+ * @state {MySQLState}
46
65
  * @returns {Operation}
47
66
  */
48
67
  export function upsert(table: string, fields: object): Operation;
49
68
  /**
50
69
  * Insert or update multiple records using ON DUPLICATE KEY
51
70
  * @public
52
- * @example <caption>Upsert multiple records</caption>
71
+ * @example<caption>Upsert multiple records into a table</caption>
53
72
  * upsertMany(
54
- * 'users', // the DB table
73
+ * "users", // the DB table
55
74
  * [
56
- * { name: 'one', email: 'one@openfn.org' },
57
- * { name: 'two', email: 'two@openfn.org' },
75
+ * { name: "one", email: "one@openfn.org" },
76
+ * { name: "two", email: "two@openfn.org" },
58
77
  * ]
59
- * )
78
+ * );
60
79
  * @function
61
80
  * @public
62
81
  * @param {string} table - The target table
63
- * @param {array} data - An array of objects or a function that returns an array
82
+ * @param {array} data - An array of objects fields
83
+ * @state {MySQLState}
64
84
  * @returns {Operation}
65
85
  */
66
86
  export function upsertMany(table: string, data: any[]): Operation;
67
- export { fn, fnIf, each, merge, field, fields, cursor, dateFns, combine, dataPath, dataValue, alterState, sourceValue, arrayToString, lastReferenceValue, as } from "@openfn/language-common";
87
+ /**
88
+ * State object
89
+ */
90
+ export type MySQLState = {
91
+ /**
92
+ * - the query results object
93
+ */
94
+ data: any;
95
+ /**
96
+ * - the query result rows
97
+ */
98
+ result: any;
99
+ /**
100
+ * - the query result fields
101
+ */
102
+ fields: any;
103
+ /**
104
+ * - an array of queries executed. Queries are added if `options.writeSql` is true.
105
+ */
106
+ queries: any;
107
+ /**
108
+ * - an array of all previous data objects used in the Job
109
+ */
110
+ references: any;
111
+ };
112
+ export type sqlOptions = {
113
+ /**
114
+ * - An array of values for prepared statements.
115
+ */
116
+ values?: any[];
117
+ /**
118
+ * - If true, logs the generated SQL statement. Defaults to false.
119
+ */
120
+ writeSql?: boolean;
121
+ /**
122
+ * - If false, does not execute the SQL, just logs it and adds to state.queries. Defaults to true.
123
+ */
124
+ execute?: boolean;
125
+ };
126
+ export { fn, fnIf, each, merge, field, fields, assert, cursor, dateFns, combine, dataPath, dataValue, alterState, sourceValue, arrayToString, lastReferenceValue, as } from "@openfn/language-common";