@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,292 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+ import { newFakeJdbcResultSetMetaData } from './fakejdbcresultsetmetadata.js';
3
+ import { FakeJdbcBigDecimal } from './fakejdbcbigdecimal.js';
4
+ import { newFakeJdbcBlob } from './fakejdbcblob.js';
5
+ import { newFakeJdbcClob } from './fakejdbcclob.js';
6
+ import { newFakeJdbcArray } from './fakejdbcarray.js';
7
+ import { newFakeInputStream, newFakeReader } from '../../support/fakeinputstream.js';
8
+
9
+ class FakeJdbcResultSet {
10
+ constructor(result, statement) {
11
+ this.__fakeObjectType = 'JdbcResultSet';
12
+ this._rows = result.rows || [];
13
+ this._fields = result.fields || [];
14
+ this._currentIndex = -1;
15
+ this._isClosed = false;
16
+ this._statement = statement;
17
+ this._lastValue = null;
18
+ this._fetchSize = 0;
19
+ this._fetchDirection = 1000; // FETCH_FORWARD
20
+ this._type = 1003; // TYPE_FORWARD_ONLY
21
+ this._concurrency = 1007; // CONCUR_READ_ONLY
22
+ }
23
+
24
+ next() {
25
+ if (this._isClosed) throw new Error('ResultSet is closed.');
26
+ this._currentIndex++;
27
+ return this._currentIndex < this._rows.length;
28
+ }
29
+
30
+ _getValue(columnIdentifier) {
31
+ if (this._isClosed) throw new Error('ResultSet is closed.');
32
+ if (this._currentIndex < 0 || this._currentIndex >= this._rows.length) {
33
+ throw new Error('No current row.');
34
+ }
35
+
36
+ let index;
37
+ if (typeof columnIdentifier === 'number') {
38
+ index = columnIdentifier;
39
+ } else {
40
+ index = this.findColumn(columnIdentifier);
41
+ }
42
+
43
+ // JDBC columns are 1-indexed
44
+ const field = this._fields[index - 1];
45
+ if (!field) throw new Error(`Invalid column identifier: ${columnIdentifier}`);
46
+ const val = this._rows[this._currentIndex][field.name];
47
+ this._lastValue = val;
48
+ return val;
49
+ }
50
+
51
+ wasNull() {
52
+ return this._lastValue === null || this._lastValue === undefined;
53
+ }
54
+
55
+ absolute(row) {
56
+ if (this._isClosed) throw new Error('ResultSet is closed.');
57
+ if (row === 0) throw new Error('Rows are 1-indexed.');
58
+ if (row > 0) {
59
+ this._currentIndex = row - 1;
60
+ } else {
61
+ this._currentIndex = this._rows.length + row;
62
+ }
63
+ return this._currentIndex >= 0 && this._currentIndex < this._rows.length;
64
+ }
65
+
66
+ afterLast() {
67
+ if (this._isClosed) throw new Error('ResultSet is closed.');
68
+ this._currentIndex = this._rows.length;
69
+ }
70
+
71
+ beforeFirst() {
72
+ if (this._isClosed) throw new Error('ResultSet is closed.');
73
+ this._currentIndex = -1;
74
+ }
75
+
76
+ first() {
77
+ return this.absolute(1);
78
+ }
79
+
80
+ last() {
81
+ return this.absolute(-1);
82
+ }
83
+
84
+ previous() {
85
+ if (this._isClosed) throw new Error('ResultSet is closed.');
86
+ if (this._currentIndex > -1) {
87
+ this._currentIndex--;
88
+ }
89
+ return this._currentIndex >= 0 && this._currentIndex < this._rows.length;
90
+ }
91
+
92
+ relative(rows) {
93
+ return this.absolute(this._currentIndex + 1 + rows);
94
+ }
95
+
96
+ isAfterLast() {
97
+ if (this._isClosed) throw new Error('ResultSet is closed.');
98
+ return this._currentIndex >= this._rows.length && this._rows.length > 0;
99
+ }
100
+
101
+ isBeforeFirst() {
102
+ if (this._isClosed) throw new Error('ResultSet is closed.');
103
+ return this._currentIndex < 0 && this._rows.length > 0;
104
+ }
105
+
106
+ isFirst() {
107
+ if (this._isClosed) throw new Error('ResultSet is closed.');
108
+ return this._currentIndex === 0 && this._rows.length > 0;
109
+ }
110
+
111
+ isLast() {
112
+ if (this._isClosed) throw new Error('ResultSet is closed.');
113
+ return this._currentIndex === this._rows.length - 1 && this._rows.length > 0;
114
+ }
115
+
116
+ getRow() {
117
+ if (this._isClosed) throw new Error('ResultSet is closed.');
118
+ if (this._currentIndex < 0 || this._currentIndex >= this._rows.length) return 0;
119
+ return this._currentIndex + 1;
120
+ }
121
+
122
+ getString(columnIndex) {
123
+ const val = this._getValue(columnIndex);
124
+ return val !== null && val !== undefined ? String(val) : null;
125
+ }
126
+
127
+ getInt(columnIndex) {
128
+ const val = this._getValue(columnIndex);
129
+ return val !== null && val !== undefined ? parseInt(val, 10) : 0;
130
+ }
131
+
132
+ getLong(columnIndex) {
133
+ return this.getInt(columnIndex);
134
+ }
135
+
136
+ getShort(columnIndex) {
137
+ return this.getInt(columnIndex);
138
+ }
139
+
140
+ getBoolean(columnIndex) {
141
+ const val = this._getValue(columnIndex);
142
+ return Boolean(val);
143
+ }
144
+
145
+ getFloat(columnIndex) {
146
+ const val = this._getValue(columnIndex);
147
+ // GAS getFloat() returns IEEE 754 single-precision (32-bit float)
148
+ return val !== null && val !== undefined ? Math.fround(parseFloat(val)) : 0.0;
149
+ }
150
+
151
+ getDouble(columnIndex) {
152
+ // getDouble() returns full 64-bit precision
153
+ const val = this._getValue(columnIndex);
154
+ return val !== null && val !== undefined ? parseFloat(val) : 0.0;
155
+ }
156
+
157
+ getBigDecimal(columnIndex) {
158
+ const val = this._getValue(columnIndex);
159
+ return val !== null && val !== undefined ? new FakeJdbcBigDecimal(val) : null;
160
+ }
161
+
162
+ getDate(columnIndex) {
163
+ const val = this._getValue(columnIndex);
164
+ return val ? new Date(val) : null;
165
+ }
166
+
167
+ getTimestamp(columnIndex) {
168
+ return this.getDate(columnIndex);
169
+ }
170
+
171
+ getObject(columnIndex) {
172
+ return this._getValue(columnIndex);
173
+ }
174
+
175
+ getBytes(columnIndex) {
176
+ const val = this._getValue(columnIndex);
177
+ if (val === null || val === undefined) return null;
178
+ return Array.from(Buffer.from(val));
179
+ }
180
+
181
+ getBlob(columnIndex) {
182
+ const val = this._getValue(columnIndex);
183
+ return val !== null && val !== undefined ? newFakeJdbcBlob(val) : null;
184
+ }
185
+
186
+ getClob(columnIndex) {
187
+ const val = this._getValue(columnIndex);
188
+ return val !== null && val !== undefined ? newFakeJdbcClob(val) : null;
189
+ }
190
+
191
+ getArray(columnIndex) {
192
+ const val = this._getValue(columnIndex);
193
+ return val !== null && val !== undefined ? newFakeJdbcArray(val) : null;
194
+ }
195
+
196
+ getUnicodeStream(columnIndex) {
197
+ const val = this._getValue(columnIndex);
198
+ return val !== null && val !== undefined ? newFakeInputStream(Buffer.from(String(val), 'utf8')) : null;
199
+ }
200
+
201
+ getAsciiStream(columnIndex) {
202
+ const val = this._getValue(columnIndex);
203
+ return val !== null && val !== undefined ? newFakeInputStream(Buffer.from(String(val), 'ascii')) : null;
204
+ }
205
+
206
+ getBinaryStream(columnIndex) {
207
+ const val = this._getValue(columnIndex);
208
+ return val !== null && val !== undefined ? newFakeInputStream(Buffer.from(val)) : null;
209
+ }
210
+
211
+ getCharacterStream(columnIndex) {
212
+ const val = this._getValue(columnIndex);
213
+ return val !== null && val !== undefined ? newFakeReader(String(val)) : null;
214
+ }
215
+
216
+ getCursorName() {
217
+ if (this._isClosed) throw new Error('ResultSet is closed.');
218
+ return 'fake-cursor';
219
+ }
220
+
221
+ findColumn(columnLabel) {
222
+ if (this._isClosed) throw new Error('ResultSet is closed.');
223
+ const index = this._fields.findIndex(f => f.name.toLowerCase() === columnLabel.toLowerCase());
224
+ if (index === -1) throw new Error(`Column not found: ${columnLabel}`);
225
+ return index + 1;
226
+ }
227
+
228
+ getMetaData() {
229
+ if (this._isClosed) throw new Error('ResultSet is closed.');
230
+ return newFakeJdbcResultSetMetaData(this._fields);
231
+ }
232
+
233
+ close() {
234
+ this._isClosed = true;
235
+ this._rows = [];
236
+ this._fields = [];
237
+ }
238
+
239
+ getByte(columnIndex) {
240
+ return this.getInt(columnIndex);
241
+ }
242
+
243
+ getTime(columnIndex) {
244
+ return this.getDate(columnIndex);
245
+ }
246
+
247
+ getURL(columnIndex) {
248
+ return this.getString(columnIndex);
249
+ }
250
+
251
+ getFetchDirection() {
252
+ return this._fetchDirection;
253
+ }
254
+
255
+ getFetchSize() {
256
+ return this._fetchSize;
257
+ }
258
+
259
+ getType() {
260
+ return this._type;
261
+ }
262
+
263
+ getConcurrency() {
264
+ return this._concurrency;
265
+ }
266
+
267
+ setFetchDirection(direction) {
268
+ this._fetchDirection = direction;
269
+ }
270
+
271
+ setFetchSize(rows) {
272
+ this._fetchSize = rows;
273
+ }
274
+
275
+ getStatement() {
276
+ return this._statement;
277
+ }
278
+
279
+ getWarnings() {
280
+ return null;
281
+ }
282
+
283
+ clearWarnings() {
284
+ // No-op for fake
285
+ }
286
+
287
+ isClosed() {
288
+ return this._isClosed;
289
+ }
290
+ }
291
+
292
+ export const newFakeJdbcResultSet = (...args) => Proxies.guard(new FakeJdbcResultSet(...args));
@@ -0,0 +1,47 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+
3
+ class FakeJdbcResultSetMetaData {
4
+ constructor(fields) {
5
+ this.__fakeObjectType = 'JdbcResultSetMetaData';
6
+ this._fields = fields || [];
7
+ }
8
+
9
+ getColumnCount() {
10
+ return this._fields.length;
11
+ }
12
+
13
+ getColumnName(column) {
14
+ const field = this._fields[column - 1];
15
+ if (!field) throw new Error(`Invalid column index: ${column}`);
16
+ return field.name;
17
+ }
18
+
19
+ getColumnLabel(column) {
20
+ return this.getColumnName(column);
21
+ }
22
+
23
+ getColumnType(column) {
24
+ // Return approximate JDBC type from pg dataTypeID
25
+ const field = this._fields[column - 1];
26
+ if (!field) throw new Error(`Invalid column index: ${column}`);
27
+ // node-postgres gives type ids (OIDs) like 23 for INT4, 25 for TEXT
28
+ // Usually JDBC maps them to java.sql.Types integers. For a fake, returning the OID or a mock type integer is adequate for basic use cases.
29
+ return field.dataTypeID;
30
+ }
31
+
32
+ getColumnTypeName(column) {
33
+ const field = this._fields[column - 1];
34
+ if (!field) throw new Error(`Invalid column index: ${column}`);
35
+ return `TYPE_${field.dataTypeID}`; // basic fallback
36
+ }
37
+
38
+ getTableName(column) {
39
+ return "";
40
+ }
41
+
42
+ getSchemaName(column) {
43
+ return "";
44
+ }
45
+ }
46
+
47
+ export const newFakeJdbcResultSetMetaData = (...args) => Proxies.guard(new FakeJdbcResultSetMetaData(...args));
@@ -0,0 +1,255 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+ import { newFakeJdbcConnection } from './fakejdbcconnection.js';
3
+ import { execSync } from 'child_process';
4
+
5
+ class FakeJdbcService {
6
+ constructor() {
7
+ this.__fakeObjectType = 'Jdbc';
8
+ }
9
+
10
+ /**
11
+ * Connects to a JDBC database.
12
+ * In gas-fakes, we primarily support postgres via pg module, mapping jdbc:postgresql to it.
13
+ *
14
+ * @param {string} url The URL of the database to connect to.
15
+ * @param {string|object} user (optional) The user name to connect as, or an object of property/value pairs.
16
+ * @param {string} password (optional) The password for the user.
17
+ * @returns {JdbcConnection} A JDBC connection object.
18
+ */
19
+ getConnection(url, user, password) {
20
+ let finalUrl = url;
21
+
22
+ if (!finalUrl) {
23
+ throw new Error('Jdbc.getConnection: URL is required or DATABASE_URL must be set in environment');
24
+ }
25
+
26
+ // Explicitly merge user/password into the URL structure for gas-fakes processing
27
+ if (typeof user === 'object' && user !== null) {
28
+ // Handling info object
29
+ const info = user;
30
+ const u = info.user || info.userName;
31
+ const p = info.password;
32
+ if (u && p) {
33
+ finalUrl = this._mergeCredentials(finalUrl, u, p);
34
+ }
35
+ // Note: other info properties aren't currently merged into the URL string,
36
+ // but Syncit.fxJdbcConnect will receive the info object anyway.
37
+ } else if (user !== undefined && user !== null && password !== undefined && password !== null) {
38
+ finalUrl = this._mergeCredentials(finalUrl, user, password);
39
+ }
40
+
41
+ // Pass user (which might be the info object) and password to the connection
42
+ return newFakeJdbcConnection(finalUrl, user, password);
43
+ }
44
+
45
+ _mergeCredentials(url, user, password) {
46
+ try {
47
+ const cleanUrl = url.replace(/^jdbc:google:/, '').replace(/^jdbc:/, '');
48
+ const urlObj = new URL(cleanUrl);
49
+ urlObj.username = encodeURIComponent(String(user));
50
+ urlObj.password = encodeURIComponent(String(password));
51
+
52
+ // Restore the prefix
53
+ let prefix = "jdbc:";
54
+ if (url.startsWith("jdbc:google:")) prefix = "jdbc:google:";
55
+ return prefix + urlObj.toString();
56
+ } catch (e) {
57
+ return url; // Fallback for malformed URLs
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Connects to a Google Cloud SQL instance.
63
+ * @param {string} url The URL of the database to connect to.
64
+ * @param {string|object} user (optional) The user name to connect as, or an object of property/value pairs.
65
+ * @param {string} password (optional) The password for the user.
66
+ * @returns {JdbcConnection} A JDBC connection object.
67
+ */
68
+ getCloudSqlConnection(url, user, password) {
69
+ return this.getConnection(url, user, password);
70
+ }
71
+
72
+ __useProxy (val) {
73
+ if (!val) return false;
74
+ const normal = this.__normalConnection (val)
75
+ return normal.local.useProxy;
76
+ };
77
+
78
+ /**
79
+ * Converts Database URLs to JDBC-compatible formats for testing and local proxy usage.
80
+ * @param {string} url - Format: protocol://user:pass@host/db OR protocol://host/db?user=X&password=Y
81
+ * @return {object} - Connection metadata.
82
+ */
83
+ __normalConnection(url) {
84
+ const isProxyRunning = (instanceName) => {
85
+ try {
86
+ execSync(`pgrep -f "cloud.*sql.*proxy.*${instanceName}"`, {
87
+ stdio: "ignore",
88
+ });
89
+ return true;
90
+ } catch (e) {
91
+ return false;
92
+ }
93
+ };
94
+
95
+ const aggressiveEncode = (str) => {
96
+ return encodeURIComponent(str).replace(/[!'()*]/g, (c) => {
97
+ return "%" + c.charCodeAt(0).toString(16).toUpperCase();
98
+ });
99
+ };
100
+
101
+ const cleanUrl = url.replace(/^jdbc:google:/, "").replace(/^jdbc:/, "");
102
+
103
+ let scheme, user, pass, host, db, searchString = "";
104
+
105
+ try {
106
+ const parsed = new URL(cleanUrl);
107
+ scheme = parsed.protocol.replace(':', '');
108
+ user = parsed.username || "";
109
+ pass = parsed.password || "";
110
+ host = parsed.hostname;
111
+ const portFromUrl = parsed.port;
112
+ if (portFromUrl) {
113
+ host = `${host}:${portFromUrl}`;
114
+ }
115
+ db = parsed.pathname.replace(/^\//, '');
116
+
117
+ const remainingParams = new URLSearchParams(parsed.search);
118
+ if (!user && remainingParams.has('user')) user = remainingParams.get('user');
119
+ if (!pass && remainingParams.has('password')) pass = remainingParams.get('password');
120
+ remainingParams.delete('user');
121
+ remainingParams.delete('password');
122
+
123
+ searchString = remainingParams.toString();
124
+ if (searchString) {
125
+ db = `${db}?${searchString}`;
126
+ }
127
+ } catch (e) {
128
+ const regex = /^([^:]+):\/\/(.+):([^@]+)@([^/]+)(?:\/(.*))?/;
129
+ const match = cleanUrl.match(regex);
130
+ if (!match) throw new Error(`Format not recognized for ${cleanUrl}`);
131
+ scheme = match[1].trim();
132
+ user = match[2].trim();
133
+ pass = match[3].trim();
134
+ host = match[4].trim();
135
+ db = match[5] ? match[5].trim() : "postgres";
136
+ }
137
+
138
+ user = decodeURIComponent(user.trim());
139
+ pass = decodeURIComponent(pass.trim());
140
+ host = host.trim();
141
+
142
+ let pureDb = db ? db.trim() : "postgres";
143
+ if (pureDb.includes('?')) pureDb = pureDb.split('?')[0];
144
+
145
+ const isPostgres = scheme.toLowerCase().includes("post");
146
+ const type = isPostgres ? "pg" : "mysql";
147
+ const protocol = isPostgres ? "jdbc:postgresql://" : "jdbc:mysql://";
148
+ const defaultPort = isPostgres ? ":5432" : ":3306";
149
+
150
+ let hostWithoutPort = host.replace(/:\d+$/, "");
151
+ let isCloudSql = hostWithoutPort.includes(":");
152
+ let isGoogle = isCloudSql || url.startsWith('jdbc:google:');
153
+ let useProxy = false;
154
+ let remoteHost = host;
155
+
156
+ const cloudSqlInstanceName = hostWithoutPort;
157
+
158
+ if (isCloudSql) {
159
+ try {
160
+ const instanceParts = hostWithoutPort.split(":");
161
+ const instanceName = instanceParts[instanceParts.length - 1];
162
+ useProxy = isProxyRunning(instanceName);
163
+
164
+ const instanceInfoStr = execSync(`gcloud sql instances describe ${instanceName} --format=json`, { encoding: "utf-8", stdio: "pipe" });
165
+ const instanceInfo = JSON.parse(instanceInfoStr);
166
+ const primaryIpObj = instanceInfo.ipAddresses?.find((ip) => ip.type === "PRIMARY");
167
+ const ip = primaryIpObj ? primaryIpObj.ipAddress : null;
168
+
169
+ if (ip && ip.match(/^[0-9.]+$/)) {
170
+ remoteHost = ip;
171
+ }
172
+
173
+ if (!useProxy) {
174
+ const localIp = execSync("curl -s https://ifconfig.me", { encoding: "utf-8", stdio: "pipe" }).trim();
175
+ if (localIp && localIp.match(/^[0-9.]+$/)) {
176
+ const authorizedNetworks = instanceInfo.settings?.ipConfiguration?.authorizedNetworks || [];
177
+ if (!authorizedNetworks.some((net) => net.value === localIp || net.value === `${localIp}/32`)) {
178
+ const newNetworks = authorizedNetworks.map((n) => n.value).concat(`${localIp}/32`).join(",");
179
+ execSync(`gcloud sql instances patch ${instanceName} --authorized-networks="${newNetworks}" --quiet`, { encoding: "utf-8", stdio: "pipe" });
180
+ }
181
+ }
182
+ }
183
+ } catch (e) {}
184
+ }
185
+
186
+ if (!remoteHost.includes(":")) remoteHost += defaultPort;
187
+
188
+ const encodedUser = aggressiveEncode(user);
189
+ const encodedPass = aggressiveEncode(pass);
190
+
191
+ // Live Apps Script Java JDBC requires properly encoded URI components to prevent "Connection URL is malformed"
192
+ const gasAuthQuery = (encodedUser || encodedPass) ? `?user=${encodedUser}&password=${encodedPass}` : '';
193
+
194
+ // --- GAS --- (Rule 3: Strip ALL SSL/Tunneling parameters)
195
+ let gasUrl, gasConnectionString, gasFullConnectionString;
196
+ let isCloudSqlConnection = false;
197
+
198
+ if (isGoogle && !isPostgres) {
199
+ gasUrl = `jdbc:google:mysql://${cloudSqlInstanceName}/${pureDb}`;
200
+ gasConnectionString = gasUrl;
201
+ gasFullConnectionString = gasUrl;
202
+ isCloudSqlConnection = true;
203
+ } else {
204
+ gasUrl = `${protocol}${remoteHost}/${pureDb}`;
205
+ gasConnectionString = gasUrl;
206
+ // Single-argument getConnection is discouraged on GAS, but if used, only append credentials
207
+ gasFullConnectionString = `${protocol}${remoteHost}/${pureDb}${gasAuthQuery}`;
208
+ }
209
+
210
+ // --- LOCAL ---
211
+ let localHostAddr = useProxy ? `127.0.0.1${defaultPort}` : remoteHost;
212
+ let localSslParam = useProxy ? "ssl=false" : "";
213
+ let localAuthParams = (encodedUser || encodedPass) ? `user=${encodedUser}&password=${encodedPass}` : '';
214
+
215
+ // Add an ampersand if both auth params and ssl param exist
216
+ if (localAuthParams && localSslParam) {
217
+ localAuthParams += "&";
218
+ }
219
+
220
+ let localUrl = `${protocol}${localHostAddr}/${pureDb}`;
221
+ let localQuery = `${localAuthParams}${localSslParam}`;
222
+ let localConnectionString = `${protocol}${localHostAddr}/${pureDb}${localQuery ? '?' + localQuery : ''}`;
223
+ let localFullConnectionString = localConnectionString;
224
+
225
+ const proxyCommand = (isCloudSql || isGoogle) && cloudSqlInstanceName.includes(":") ? `cloud-sql-proxy ${cloudSqlInstanceName}` : "";
226
+
227
+ const gasObj = {
228
+ url: gasUrl,
229
+ connectionString: gasConnectionString,
230
+ user, password: pass,
231
+ fullConnectionString: gasFullConnectionString,
232
+ isCloudSqlConnection,
233
+ proxyCommand
234
+ };
235
+
236
+ const localObj = {
237
+ url: localUrl,
238
+ connectionString: localConnectionString,
239
+ user, password: pass,
240
+ fullConnectionString: localFullConnectionString,
241
+ useProxy, proxyCommand
242
+ };
243
+
244
+ return {
245
+ current: (typeof ScriptApp !== 'undefined' && ScriptApp.isFake) ? 'local' : 'gas',
246
+ gas: gasObj,
247
+ local: localObj,
248
+ host: remoteHost,
249
+ proxyCommand,
250
+ isGoogle, type
251
+ };
252
+ }
253
+ }
254
+
255
+ export const newFakeJdbcService = (...args) => Proxies.guard(new FakeJdbcService(...args));