bulltrackers-module 1.0.1069 → 1.0.1071

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.
@@ -1,4 +1,5 @@
1
1
  const { recordAnalyticsWrite } = require('../../../core/utils/analytics_registry');
2
+ const { ensureWatchlistsFirestoreBigQueryExport } = require('../../../core/utils/bigquery_utils');
2
3
  const { resolveFirestorePaths } = require('../../config/firestorePaths');
3
4
  const {
4
5
  validateUserId,
@@ -90,6 +91,8 @@ class WatchlistWriteService {
90
91
  if (firestoreData.tags !== undefined && Array.isArray(firestoreData.tags)) firestoreData.tags = firestoreData.tags.map(t => sanitizeString(t));
91
92
  Object.keys(firestoreData).forEach(k => { if (firestoreData[k] === undefined) delete firestoreData[k]; });
92
93
 
94
+ await ensureWatchlistsFirestoreBigQueryExport();
95
+
93
96
  const docRef = this.db.collection(this.c.signedInUsers).doc(validUserId).collection(this.sc.watchlists).doc(watchlistId);
94
97
  const oldDoc = await docRef.get();
95
98
  const created = !oldDoc.exists;
@@ -105,6 +108,7 @@ class WatchlistWriteService {
105
108
  const validUserId = validateUserId(userId);
106
109
  const validWatchlistId = validateId(watchlistId, 'Watchlist ID');
107
110
  await assertUserOwnsDocument(this.db, this.c, this.sc, validUserId, 'watchlists', validWatchlistId);
111
+ await ensureWatchlistsFirestoreBigQueryExport();
108
112
  const docRef = this.db.collection(this.c.signedInUsers).doc(validUserId).collection(this.sc.watchlists).doc(validWatchlistId);
109
113
  const oldDoc = await docRef.get();
110
114
  const oldItems = oldDoc.exists ? (oldDoc.data().items || []) : [];
@@ -11,17 +11,121 @@ const os = require('os');
11
11
  // Singleton BigQuery client
12
12
  let bigqueryClient = null;
13
13
 
14
+ /** After a successful ensure, skip repeated BigQuery metadata checks on watchlist writes. */
15
+ let watchlistsFirestoreExportEnsured = false;
16
+
14
17
  /**
15
18
  * Get or create BigQuery client
16
19
  */
17
20
  function getBigQueryClient() {
18
21
  if (!bigqueryClient) {
19
- const projectId = process.env.GCP_PROJECT_ID || 'stocks-12345';
22
+ const projectId = process.env.GCP_PROJECT_ID || process.env.GOOGLE_CLOUD_PROJECT || 'stocks-12345';
20
23
  bigqueryClient = new BigQuery({ projectId });
21
24
  }
22
25
  return bigqueryClient;
23
26
  }
24
27
 
28
+ /** Dataset where Firebase `firestore-bigquery-export` instances write (`bt_core` in prod). */
29
+ function getFirestoreBigQueryExportDatasetId() {
30
+ return process.env.BQ_BT_CORE_DATASET_ID || 'bt_core';
31
+ }
32
+
33
+ async function runBqDdl(sql) {
34
+ const bigquery = getBigQueryClient();
35
+ const [job] = await bigquery.createQueryJob({ query: sql, location: 'europe-west1' });
36
+ await job.getQueryResults();
37
+ }
38
+
39
+ /**
40
+ * If the extension never created `watchlists_sync_raw_changelog` / `_raw_latest`, create them by
41
+ * cloning schema from another working export in the same dataset (same extension version/settings).
42
+ * Call before Firestore writes so the extension trigger finds the changelog table.
43
+ *
44
+ * Never throws — logs and returns so user writes still succeed.
45
+ */
46
+ async function ensureWatchlistsFirestoreBigQueryExport(logger = null) {
47
+ if (watchlistsFirestoreExportEnsured) return;
48
+ if (process.env.SKIP_FIRESTORE_BQ_EXPORT_ENSURE === '1' || process.env.NODE_ENV === 'test') return;
49
+ if (process.env.BIGQUERY_ENABLED === 'false') return;
50
+
51
+ const projectId = process.env.GCP_PROJECT_ID || process.env.GOOGLE_CLOUD_PROJECT || 'stocks-12345';
52
+ const datasetId = getFirestoreBigQueryExportDatasetId();
53
+ const targetBase = 'watchlists_sync';
54
+ const changelogTarget = `${targetBase}_raw_changelog`;
55
+ const latestTarget = `${targetBase}_raw_latest`;
56
+
57
+ const referenceBases = [
58
+ 'alert_subscriptions_sync',
59
+ 'user_settings_sync',
60
+ 'fcm_tokens_sync',
61
+ 'reviews_sync',
62
+ ];
63
+
64
+ const log = (level, msg, extra) => {
65
+ if (logger && typeof logger.log === 'function') logger.log(level, msg, extra);
66
+ else if (level === 'WARN' || level === 'ERROR') console.warn(msg, extra || '');
67
+ };
68
+
69
+ try {
70
+ const bigquery = getBigQueryClient();
71
+ const dataset = bigquery.dataset(datasetId);
72
+
73
+ const [changelogExists] = await dataset.table(changelogTarget).exists();
74
+ const [latestExists] = await dataset.table(latestTarget).exists();
75
+ if (changelogExists && latestExists) {
76
+ watchlistsFirestoreExportEnsured = true;
77
+ return;
78
+ }
79
+
80
+ let referenceBase = null;
81
+ for (const base of referenceBases) {
82
+ const [refExists] = await dataset.table(`${base}_raw_changelog`).exists();
83
+ if (refExists) {
84
+ referenceBase = base;
85
+ break;
86
+ }
87
+ }
88
+ if (!referenceBase) {
89
+ log('WARN', `[BigQuery] ensureWatchlistsFirestoreBigQueryExport: no reference *_raw_changelog in ${datasetId} (skipped)`);
90
+ return;
91
+ }
92
+
93
+ const refChangelog = `${referenceBase}_raw_changelog`;
94
+ const fq = (tid) => `\`${projectId}.${datasetId}.${tid}\``;
95
+
96
+ if (!changelogExists) {
97
+ await runBqDdl(`CREATE TABLE ${fq(changelogTarget)} LIKE ${fq(refChangelog)}`);
98
+ log('INFO', `[BigQuery] Created ${datasetId}.${changelogTarget} LIKE ${refChangelog}`);
99
+ }
100
+
101
+ const [latestExistsAfter] = await dataset.table(latestTarget).exists();
102
+ if (!latestExistsAfter) {
103
+ const viewSql = `
104
+ CREATE VIEW ${fq(latestTarget)} AS
105
+ SELECT * EXCEPT (rank)
106
+ FROM (
107
+ SELECT
108
+ *,
109
+ ROW_NUMBER() OVER (PARTITION BY document_name ORDER BY timestamp DESC) AS rank
110
+ FROM ${fq(changelogTarget)}
111
+ )
112
+ WHERE rank = 1
113
+ `;
114
+ await runBqDdl(viewSql);
115
+ log('INFO', `[BigQuery] Created view ${datasetId}.${latestTarget}`);
116
+ }
117
+
118
+ watchlistsFirestoreExportEnsured = true;
119
+ } catch (err) {
120
+ const msg = err && err.message ? err.message : String(err);
121
+ if (/Already exists|duplicate/i.test(msg)) {
122
+ watchlistsFirestoreExportEnsured = true;
123
+ return;
124
+ }
125
+ log('WARN', `[BigQuery] ensureWatchlistsFirestoreBigQueryExport failed (watchlist write continues): ${msg}`);
126
+ }
127
+ }
128
+
25
129
  /**
26
130
  * Get dataset reference, creating it if it doesn't exist
27
131
  * @param {string} datasetId - Dataset ID (e.g., 'bulltrackers_data')
@@ -2422,6 +2526,8 @@ async function queryPIAlertHistory(dateStr, logger = null) {
2422
2526
 
2423
2527
  module.exports = {
2424
2528
  getBigQueryClient,
2529
+ getFirestoreBigQueryExportDatasetId,
2530
+ ensureWatchlistsFirestoreBigQueryExport,
2425
2531
  getOrCreateDataset,
2426
2532
  ensureTableExists,
2427
2533
  insertRows,
@@ -19,6 +19,26 @@ or CLI (`firebase ext:install firebase/firestore-bigquery-export`).
19
19
  | Max enqueue attempts | `3` |
20
20
  | Log level | `Info` |
21
21
 
22
+ ## How BigQuery objects get created
23
+
24
+ On each Firestore write, the extension’s Cloud Function runs **`initialize()` first** (unless disabled). That path creates the dataset if needed, the **`{TABLE_ID}_raw_changelog`** table, and the **`{TABLE_ID}_raw_latest`** view, then streams the row. So you should **not** need to create `watchlists_sync_raw_changelog` by hand.
25
+
26
+ If you see **`Not found: Table …watchlists_sync_raw_changelog`** while **`alert_subscriptions_sync_raw_changelog`** (etc.) already exist, initialization **never completed successfully for that instance**—not “the extension skips creating missing tables.” Typical causes:
27
+
28
+ 1. **BigQuery IAM for this instance’s service account**
29
+ Each extension instance uses its **own** service account (Firebase Console → Extensions → instance → **Managed service account**). It must be able to create tables in `bt_core` (usually **`roles/bigquery.dataEditor`** on dataset `bt_core`, or project-level editor). Other instances can work while watchlists fails if this SA was never granted or was recreated.
30
+
31
+ 2. **`initialize()` failed earlier**
32
+ Check **Logs** for the same function **before** your latest write (permission errors, API errors, wrong `BIGQUERY_PROJECT_ID` / `DATASET_ID` / `TABLE_ID`). Fix config or IAM, then trigger another write.
33
+
34
+ 3. **Wrong `TABLE_ID` in the Console**
35
+ Must be exactly `watchlists_sync` so BigQuery names are `watchlists_sync_raw_changelog` and `watchlists_sync_raw_latest`.
36
+
37
+ 4. **Changelog removed manually**
38
+ Recreate by reinstalling the extension or running the backfill tool below (it creates the destination schema).
39
+
40
+ **Recovery:** Grant the instance SA **`roles/bigquery.dataEditor`** on `bt_core`, then save a watchlist again **or** run the backfill command for watchlists (creates/aligns tables). If still broken, uninstall and reinstall the watchlists extension with the same parameters as this doc.
41
+
22
42
  ## Instance 1: bt-watchlists
23
43
 
24
44
  | Parameter | Value |
@@ -1 +1 @@
1
- const version = 23.0
1
+ const version = 24.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.1069",
3
+ "version": "1.0.1071",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [