@mcpher/gas-fakes 2.3.4 → 2.3.6

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.
@@ -0,0 +1,185 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+ import { Syncit } from '../../support/syncit.js';
3
+ import { newFakeJdbcResultSet } from './fakejdbcresultset.js';
4
+
5
+ class FakeJdbcStatement {
6
+ constructor(connection, connectionId) {
7
+ this.__fakeObjectType = 'JdbcStatement';
8
+ this._connection = connection;
9
+ this._connectionId = connectionId;
10
+ this._isClosed = false;
11
+ this._batch = [];
12
+ this._fetchSize = 0;
13
+ this._maxRows = 0;
14
+ this._queryTimeout = 0;
15
+ this._maxFieldSize = 0;
16
+ this._fetchDirection = 1000; // FETCH_FORWARD
17
+ this._isPoolable = false;
18
+ this._lastUpdateCount = -1;
19
+ }
20
+
21
+ addBatch(sql) {
22
+ if (this._isClosed) throw new Error('Statement is closed.');
23
+ this._batch.push(sql);
24
+ }
25
+
26
+ executeBatch() {
27
+ if (this._isClosed) throw new Error('Statement is closed.');
28
+ const results = [];
29
+ for (const sql of this._batch) {
30
+ const result = Syncit.fxJdbcQuery(this._connectionId, sql);
31
+ results.push(result.rowCount || 0);
32
+ }
33
+ this.clearBatch();
34
+ return results;
35
+ }
36
+
37
+ clearBatch() {
38
+ this._batch = [];
39
+ }
40
+
41
+ executeQuery(sql) {
42
+ if (this._isClosed) throw new Error('Statement is closed.');
43
+
44
+ // Fetch result synchronously from worker
45
+ const result = Syncit.fxJdbcQuery(this._connectionId, sql);
46
+ this._lastUpdateCount = -1;
47
+ this._lastResultSet = newFakeJdbcResultSet(result, this);
48
+ return this._lastResultSet;
49
+ }
50
+
51
+ execute(sql, autoGeneratedKeysOrIndexesOrNames) {
52
+ if (this._isClosed) throw new Error('Statement is closed.');
53
+
54
+ const result = Syncit.fxJdbcQuery(this._connectionId, sql);
55
+ this._lastUpdateCount = result.rowCount || 0;
56
+ if (result.fields && result.fields.length > 0) {
57
+ this._lastResultSet = newFakeJdbcResultSet(result, this);
58
+ return true;
59
+ } else {
60
+ this._lastResultSet = null;
61
+ return false;
62
+ }
63
+ }
64
+
65
+ executeUpdate(sql, autoGeneratedKeysOrIndexesOrNames) {
66
+ if (this._isClosed) throw new Error('Statement is closed.');
67
+ const result = Syncit.fxJdbcQuery(this._connectionId, sql);
68
+ this._lastUpdateCount = result.rowCount;
69
+ this._lastResultSet = null;
70
+ return result.rowCount;
71
+ }
72
+
73
+ cancel() {
74
+ // Stub: No-op for fake
75
+ }
76
+
77
+ clearWarnings() {
78
+ // Stub: No-op for fake
79
+ }
80
+
81
+ getConnection() {
82
+ return this._connection;
83
+ }
84
+
85
+ getFetchDirection() {
86
+ return this._fetchDirection;
87
+ }
88
+
89
+ getFetchSize() {
90
+ return this._fetchSize;
91
+ }
92
+
93
+ getGeneratedKeys() {
94
+ // Return empty results as we don't currently track generated auto-increment IDs
95
+ return newFakeJdbcResultSet({ fields: [], rows: [] }, this);
96
+ }
97
+
98
+ getMaxFieldSize() {
99
+ return this._maxFieldSize;
100
+ }
101
+
102
+ getMaxRows() {
103
+ return this._maxRows;
104
+ }
105
+
106
+ getMoreResults(current) {
107
+ // We only support one result set at a time per statement in this fake
108
+ return false;
109
+ }
110
+
111
+ getQueryTimeout() {
112
+ return this._queryTimeout;
113
+ }
114
+
115
+ getResultSetConcurrency() {
116
+ return 1007; // CONCUR_READ_ONLY
117
+ }
118
+
119
+ getResultSetHoldability() {
120
+ return 1; // HOLD_CURSORS_OVER_COMMIT
121
+ }
122
+
123
+ getResultSetType() {
124
+ return 1003; // TYPE_FORWARD_ONLY
125
+ }
126
+
127
+ getWarnings() {
128
+ return null;
129
+ }
130
+
131
+ isPoolable() {
132
+ return this._isPoolable;
133
+ }
134
+
135
+ setCursorName(name) {
136
+ // Stub: No-op for fake
137
+ }
138
+
139
+ setEscapeProcessing(enable) {
140
+ // Stub: No-op for fake
141
+ }
142
+
143
+ setFetchDirection(direction) {
144
+ this._fetchDirection = direction;
145
+ }
146
+
147
+ setFetchSize(rows) {
148
+ this._fetchSize = rows;
149
+ }
150
+
151
+ setMaxFieldSize(max) {
152
+ this._maxFieldSize = max;
153
+ }
154
+
155
+ setMaxRows(max) {
156
+ this._maxRows = max;
157
+ }
158
+
159
+ setPoolable(poolable) {
160
+ this._isPoolable = poolable;
161
+ }
162
+
163
+ setQueryTimeout(seconds) {
164
+ this._queryTimeout = seconds;
165
+ }
166
+
167
+ getUpdateCount() {
168
+ return this._lastUpdateCount || 0;
169
+ }
170
+
171
+ getResultSet() {
172
+ return this._lastResultSet;
173
+ }
174
+
175
+ close() {
176
+ this._isClosed = true;
177
+ this._lastResultSet = null;
178
+ }
179
+
180
+ isClosed() {
181
+ return this._isClosed;
182
+ }
183
+ }
184
+
185
+ export const newFakeJdbcStatement = (...args) => Proxies.guard(new FakeJdbcStatement(...args));
@@ -0,0 +1,42 @@
1
+ import { Proxies } from './proxies.js';
2
+
3
+ class FakeInputStream {
4
+ constructor(data) {
5
+ this.__fakeObjectType = 'InputStream';
6
+ this._data = data instanceof Uint8Array ? data : Buffer.from(data || '');
7
+ this._pos = 0;
8
+ }
9
+
10
+ read() {
11
+ if (this._pos >= this._data.length) return -1;
12
+ return this._data[this._pos++];
13
+ }
14
+
15
+ available() {
16
+ return this._data.length - this._pos;
17
+ }
18
+
19
+ close() {
20
+ this._pos = this._data.length;
21
+ }
22
+ }
23
+
24
+ class FakeReader {
25
+ constructor(data) {
26
+ this.__fakeObjectType = 'Reader';
27
+ this._data = String(data || '');
28
+ this._pos = 0;
29
+ }
30
+
31
+ read() {
32
+ if (this._pos >= this._data.length) return -1;
33
+ return this._data.charCodeAt(this._pos++);
34
+ }
35
+
36
+ close() {
37
+ this._pos = this._data.length;
38
+ }
39
+ }
40
+
41
+ export const newFakeInputStream = (...args) => Proxies.guard(new FakeInputStream(...args));
42
+ export const newFakeReader = (...args) => Proxies.guard(new FakeReader(...args));
@@ -0,0 +1,16 @@
1
+ import { createRequire } from 'node:module';
2
+ const require = createRequire(import.meta.url);
3
+
4
+ /**
5
+ * Initializes the global GasFakes metadata object.
6
+ * @param {string} packageJsonPath The relative path to package.json from this file.
7
+ * @returns {object} The GasFakes global object.
8
+ */
9
+ export function initMetadata(packageJsonPath = '../../package.json') {
10
+ // add the version number to gasfakes metadata
11
+ if (!globalThis.GasFakes) globalThis.GasFakes = {};
12
+ const pjson = require(packageJsonPath);
13
+ if (!globalThis.GasFakes.metadata) globalThis.GasFakes.metadata = {}
14
+ globalThis.GasFakes.metadata.version = pjson.version
15
+ return globalThis.GasFakes;
16
+ }
@@ -128,6 +128,32 @@ export const sxInit = async ({ manifestPath, claspPath, settingsPath, cachePath,
128
128
 
129
129
  } catch (err) {
130
130
  syncWarn(`Google authentication failed: ${err.message}`);
131
+
132
+ // Provide guidance for Domain Wide Delegation issues
133
+ if (err.message.includes('unauthorized_client')) {
134
+ const clientId = Auth.getClientId();
135
+ const msg = [
136
+ "",
137
+ "=".repeat(80),
138
+ "GOOGLE AUTHENTICATION ERROR: unauthorized_client",
139
+ "This usually means Domain-Wide Delegation (DWD) is missing for one or more scopes.",
140
+ "",
141
+ `Your Service Account Client ID is: ${clientId || 'unknown (check your service account JSON file)'}`,
142
+ "",
143
+ "The following scopes should be authorized in the Google Admin Console:",
144
+ finalScopes.join(","),
145
+ "",
146
+ "To fix this:",
147
+ "1. Go to https://admin.google.com",
148
+ "2. Security -> Access and data control -> API controls",
149
+ "3. Manage Domain Wide Delegation",
150
+ "4. Find/Add your Client ID and ensure the list of scopes above matches exactly.",
151
+ "=".repeat(80),
152
+ ""
153
+ ].join("\n");
154
+ console.error(msg);
155
+ }
156
+
131
157
  if (!platforms.includes('ksuite') && !platforms.includes('msgraph')) throw err;
132
158
  }
133
159
  }
@@ -0,0 +1,234 @@
1
+ import pg from 'pg';
2
+ import mysql from 'mysql2/promise';
3
+
4
+ const connections = new Map();
5
+
6
+ /**
7
+ * Creates and stores a connection to the database.
8
+ * @param {import('./auth.js').Auth} Auth
9
+ * @param {object} params
10
+ * @param {string} params.url JDBC connection URL
11
+ * @param {string} params.user Optional username
12
+ * @param {string} params.password Optional password
13
+ * @returns {object} Connection details/ID
14
+ */
15
+ export const sxJdbcConnect = async (Auth, { url, user, password }) => {
16
+ let connectionString = url;
17
+ let type = 'pg';
18
+
19
+ if (url.startsWith('jdbc:postgresql:')) {
20
+ connectionString = url.replace('jdbc:postgresql:', 'postgresql:');
21
+ type = 'pg';
22
+ } else if (url.startsWith('jdbc:mysql:')) {
23
+ connectionString = url.replace('jdbc:mysql:', 'mysql:');
24
+ type = 'mysql';
25
+ } else if (url.startsWith('jdbc:google:mysql:')) {
26
+ connectionString = url.replace('jdbc:google:mysql:', 'mysql:');
27
+ type = 'mysql';
28
+ } else if (url.startsWith('mysql:')) {
29
+ type = 'mysql';
30
+ }
31
+
32
+ // Parse custom flags
33
+ const disableSsl = connectionString.includes('ssl=false') || connectionString.includes('127.0.0.1');
34
+
35
+ let client;
36
+ const id = `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
37
+
38
+ if (type === 'pg') {
39
+ // Strip ssl parameters to avoid conflict with explicit ssl object
40
+ let pgConnectionString = connectionString.replace(/([?&])ssl=[^&]*(&|$)/g, '$1').replace(/[?&]$/, '');
41
+
42
+ // Neon postgres usually requires ssl
43
+ if (pgConnectionString.includes('sslmode=require') && !pgConnectionString.includes('uselibpqcompat')) {
44
+ const separator = pgConnectionString.includes('?') ? '&' : '?';
45
+ pgConnectionString += `${separator}uselibpqcompat=true`;
46
+ }
47
+
48
+ const clientConfig = {
49
+ connectionString: pgConnectionString,
50
+ ssl: disableSsl ? false : { rejectUnauthorized: false }
51
+ };
52
+
53
+ if (user !== null && typeof user !== 'undefined' && password !== null && typeof password !== 'undefined') {
54
+ client = new pg.Client({
55
+ ...clientConfig,
56
+ user: String(user),
57
+ password: String(password)
58
+ });
59
+ } else {
60
+ client = new pg.Client(clientConfig);
61
+ }
62
+ await client.connect();
63
+ } else if (type === 'mysql') {
64
+ let cleanUrl = connectionString.replace(/^jdbc:/, '');
65
+ if (!cleanUrl.startsWith('mysql://')) {
66
+ cleanUrl = 'mysql://' + cleanUrl;
67
+ }
68
+
69
+ let host = '127.0.0.1';
70
+ let port = 3306;
71
+ let database = '';
72
+ let urlUser;
73
+ let urlPassword;
74
+
75
+ try {
76
+ const parsedUrl = new URL(cleanUrl);
77
+ host = parsedUrl.hostname;
78
+ port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : 3306;
79
+ database = parsedUrl.pathname.replace(/^\//, '');
80
+ urlUser = parsedUrl.username ? decodeURIComponent(parsedUrl.username) : parsedUrl.searchParams.get('user');
81
+ urlPassword = parsedUrl.password ? decodeURIComponent(parsedUrl.password) : parsedUrl.searchParams.get('password');
82
+ } catch (e) {
83
+ // Fallback for weird formats (like Cloud SQL instance names without resolved IPs)
84
+ const match = cleanUrl.match(/^mysql:\/\/(?:([^:]+):([^@]+)@)?([^/]+?)(?::(\d+))?(?:\/([^?]+))?(?:\?(.*))?$/);
85
+ if (match) {
86
+ urlUser = match[1] ? decodeURIComponent(match[1]) : undefined;
87
+ urlPassword = match[2] ? decodeURIComponent(match[2]) : undefined;
88
+ host = match[3];
89
+ port = match[4] ? parseInt(match[4], 10) : 3306;
90
+ database = match[5] || '';
91
+ } else {
92
+ host = cleanUrl.replace(/^mysql:\/\//, '');
93
+ }
94
+ }
95
+
96
+ const finalUser = (user !== null && typeof user !== 'undefined') ? String(user) : urlUser;
97
+ const finalPassword = (password !== null && typeof password !== 'undefined') ? String(password) : urlPassword;
98
+
99
+ const mysqlConfig = {
100
+ host: host,
101
+ port: port,
102
+ database: database,
103
+ user: finalUser,
104
+ password: finalPassword,
105
+ ssl: disableSsl ? undefined : { rejectUnauthorized: false }
106
+ };
107
+
108
+ client = await mysql.createConnection(mysqlConfig);
109
+ }
110
+
111
+ connections.set(id, { client, type });
112
+ return { id };
113
+ };
114
+
115
+ /**
116
+ * Executes a query on a given connection.
117
+ * @param {import('./auth.js').Auth} Auth
118
+ * @param {object} params
119
+ * @param {string} params.connectionId The stored connection ID
120
+ * @param {string} params.sql The SQL query to execute
121
+ * @returns {object} The query result (rows, fields, etc.)
122
+ */
123
+ export const sxJdbcQuery = async (Auth, { connectionId, sql }) => {
124
+ const entry = connections.get(connectionId);
125
+ if (!entry) throw new Error('Invalid or closed JDBC connection.');
126
+
127
+ const { client, type } = entry;
128
+
129
+ if (type === 'pg') {
130
+ const result = await client.query(sql);
131
+ const rows = Array.isArray(result) ? result[result.length - 1].rows : result.rows;
132
+ const fields = Array.isArray(result) ? result[result.length - 1].fields : result.fields;
133
+ const rowCount = Array.isArray(result) ? result[result.length - 1].rowCount : result.rowCount;
134
+
135
+ return {
136
+ rows: rows || [],
137
+ fields: fields || [],
138
+ rowCount: rowCount || 0,
139
+ };
140
+ } else if (type === 'mysql') {
141
+ // mysql2 returns [rows, fields]
142
+ const [rows, fields] = await client.query(sql);
143
+ return {
144
+ rows: rows || [],
145
+ fields: fields || [], // mysql2 fields are objects with name, columnType, etc.
146
+ rowCount: (rows && rows.insertId) ? 1 : (rows && rows.affectedRows) || (rows && rows.length) || 0,
147
+ };
148
+ }
149
+ };
150
+
151
+ /**
152
+ * Executes a prepared statement on a given connection.
153
+ * @param {import('./auth.js').Auth} Auth
154
+ * @param {object} params
155
+ * @param {string} params.connectionId The stored connection ID
156
+ * @param {string} params.sql The SQL query with placeholders
157
+ * @param {any[]} params.values The parameter values
158
+ * @returns {object} The query result
159
+ */
160
+ export const sxJdbcExecutePrepared = async (Auth, { connectionId, sql, values }) => {
161
+ const entry = connections.get(connectionId);
162
+ if (!entry) throw new Error('Invalid or closed JDBC connection.');
163
+
164
+ const { client, type } = entry;
165
+
166
+ if (type === 'pg') {
167
+ // pg uses $1, $2, etc. so we must convert ? to $n
168
+ let index = 1;
169
+ const pgSql = sql.replace(/\?/g, () => `$${index++}`);
170
+ const result = await client.query(pgSql, values);
171
+ return {
172
+ rows: result.rows || [],
173
+ fields: result.fields || [],
174
+ rowCount: result.rowCount || 0,
175
+ };
176
+ } else if (type === 'mysql') {
177
+ // mysql2 natively supports ? placeholders
178
+ const [rows, fields] = await client.execute(sql, values);
179
+ return {
180
+ rows: rows || [],
181
+ fields: fields || [],
182
+ rowCount: (rows && (rows.affectedRows || rows.length)) || 0,
183
+ };
184
+ }
185
+ };
186
+
187
+ export const sxJdbcCommit = async (Auth, { connectionId }) => {
188
+ return sxJdbcQuery(Auth, { connectionId, sql: 'COMMIT' });
189
+ };
190
+
191
+ export const sxJdbcRollback = async (Auth, { connectionId }) => {
192
+ return sxJdbcQuery(Auth, { connectionId, sql: 'ROLLBACK' });
193
+ };
194
+
195
+ /**
196
+ * Sets auto-commit mode for the connection.
197
+ * Note: Node drivers are technically always in a state where BEGIN is needed for transactions.
198
+ * @param {import('./auth.js').Auth} Auth
199
+ * @param {object} params
200
+ * @param {string} params.connectionId The stored connection ID
201
+ * @param {boolean} params.autoCommit Whether to enable auto-commit
202
+ */
203
+ export const sxJdbcSetAutoCommit = async (Auth, { connectionId, autoCommit }) => {
204
+ const entry = connections.get(connectionId);
205
+ if (!entry) throw new Error('Invalid or closed JDBC connection.');
206
+
207
+ if (!autoCommit) {
208
+ // If turning off auto-commit, start a transaction
209
+ await sxJdbcQuery(Auth, { connectionId, sql: 'BEGIN' });
210
+ }
211
+ entry.autoCommit = autoCommit;
212
+ return true;
213
+ };
214
+
215
+ /**
216
+ * Closes a given connection.
217
+ * @param {import('./auth.js').Auth} Auth
218
+ * @param {object} params
219
+ * @param {string} params.connectionId The stored connection ID
220
+ * @returns {boolean} True if closed successfully
221
+ */
222
+ export const sxJdbcClose = async (Auth, { connectionId }) => {
223
+ const entry = connections.get(connectionId);
224
+ if (entry) {
225
+ const { client, type } = entry;
226
+ if (type === 'pg') {
227
+ await client.end();
228
+ } else if (type === 'mysql') {
229
+ await client.end();
230
+ }
231
+ connections.delete(connectionId);
232
+ }
233
+ return true;
234
+ };
@@ -412,6 +412,45 @@ const fxTestRetry = (errorMessage) => {
412
412
  return safeCallSync("sxTestRetry", { errorMessage });
413
413
  };
414
414
 
415
+ const fxJdbcConnect = (url, user, password) => {
416
+ const args = { url };
417
+ if (typeof user === 'object' && user !== null) {
418
+ // Handling info object
419
+ args.user = user.user || user.userName;
420
+ args.password = user.password;
421
+ // We could pass the whole object if the worker was ready for it,
422
+ // but for now let's just stick to user/pass.
423
+ } else {
424
+ if (user !== null && typeof user !== 'undefined') args.user = user;
425
+ if (password !== null && typeof password !== 'undefined') args.password = password;
426
+ }
427
+ return safeCallSync("sxJdbcConnect", args);
428
+ };
429
+
430
+ const fxJdbcQuery = (connectionId, sql) => {
431
+ return safeCallSync("sxJdbcQuery", { connectionId, sql });
432
+ };
433
+
434
+ const fxJdbcExecutePrepared = (connectionId, sql, values) => {
435
+ return safeCallSync("sxJdbcExecutePrepared", { connectionId, sql, values });
436
+ };
437
+
438
+ const fxJdbcCommit = (connectionId) => {
439
+ return safeCallSync("sxJdbcCommit", { connectionId });
440
+ };
441
+
442
+ const fxJdbcRollback = (connectionId) => {
443
+ return safeCallSync("sxJdbcRollback", { connectionId });
444
+ };
445
+
446
+ const fxJdbcSetAutoCommit = (connectionId, autoCommit) => {
447
+ return safeCallSync("sxJdbcSetAutoCommit", { connectionId, autoCommit });
448
+ };
449
+
450
+ const fxJdbcClose = (connectionId) => {
451
+ return safeCallSync("sxJdbcClose", { connectionId });
452
+ };
453
+
415
454
  const fxSheets = (args) =>
416
455
  fxGeneric({
417
456
  ...args,
@@ -479,5 +518,12 @@ export const Syncit = {
479
518
  fxGetAccessToken,
480
519
  fxGetAccessTokenInfo,
481
520
  fxGetSourceAccessTokenInfo,
482
- fxTestRetry
521
+ fxTestRetry,
522
+ fxJdbcConnect,
523
+ fxJdbcQuery,
524
+ fxJdbcExecutePrepared,
525
+ fxJdbcCommit,
526
+ fxJdbcRollback,
527
+ fxJdbcSetAutoCommit,
528
+ fxJdbcClose
483
529
  }
@@ -14,3 +14,4 @@ export * from "../sxtoken.js";
14
14
 
15
15
 
16
16
 
17
+ export * from "../sxjdbc.js";