Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion apps/region-pages/.env.local.sample
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# Copy this file to .env.local and fill in your actual values
POSTGRES_URL="postgresql://postgres:postgres@127.0.0.1:54322/postgres"
F3_DATA_WAREHOUSE_URL="postgresql://[user]:[password]@[host]:[port]/[database]"
F3_DATA_WAREHOUSE_URL="postgresql://[user]:[password]@[host]:[port]/[database]"

# Cloud SQL Connector (set WAREHOUSE_DB_CONNECTION_MODE=connector to use instead of F3_DATA_WAREHOUSE_URL)
# WAREHOUSE_DB_CONNECTION_MODE="direct"
# CLOUD_SQL_WAREHOUSE_CONNECTION_NAME="project:region:instance"
# WAREHOUSE_DB_USER=""
# WAREHOUSE_DB_PASSWORD=""
# WAREHOUSE_DB_NAME=""
# CLOUD_SQL_WAREHOUSE_IP_TYPE="PUBLIC"
24 changes: 24 additions & 0 deletions apps/region-pages/apphosting.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,30 @@ env:
- BUILD
- RUNTIME

# Cloud SQL Connector settings (set WAREHOUSE_DB_CONNECTION_MODE=connector to use)
- variable: WAREHOUSE_DB_CONNECTION_MODE
value: connector

- variable: CLOUD_SQL_WAREHOUSE_CONNECTION_NAME
secret: cloud-sql-warehouse-connection-name
availability:
- RUNTIME

- variable: WAREHOUSE_DB_USER
secret: warehouse-db-user
availability:
- RUNTIME

- variable: WAREHOUSE_DB_PASSWORD
secret: warehouse-db-password
availability:
- RUNTIME

- variable: WAREHOUSE_DB_NAME
secret: warehouse-db-name
availability:
- RUNTIME

# Grant access to secrets in Cloud Secret Manager.
# See https://firebase.google.com/docs/app-hosting/configure#secret-parameters
# - variable: MY_SECRET
Expand Down
122 changes: 111 additions & 11 deletions apps/region-pages/drizzle/f3-data-warehouse/db.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,118 @@
import { drizzle } from 'drizzle-orm/node-postgres';
import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import type { IpAddressTypes } from '@google-cloud/cloud-sql-connector';
import { loadEnvConfig } from '@/lib/env';
import * as schema from './schema';

// Load environment variables
const { F3_DATA_WAREHOUSE_URL } = loadEnvConfig();
let pool: Pool | null = null;
let connector: InstanceType<
typeof import('@google-cloud/cloud-sql-connector').Connector
> | null = null;
let dbInstance: NodePgDatabase<typeof schema> | null = null;

// Create PostgreSQL connection pool
const pool = new Pool({
connectionString: F3_DATA_WAREHOUSE_URL,
});
/**
* Creates a pool using the Cloud SQL Node.js Connector.
* Requires: CLOUD_SQL_WAREHOUSE_CONNECTION_NAME, WAREHOUSE_DB_USER, WAREHOUSE_DB_NAME
*/
async function createCloudSqlPool(): Promise<Pool> {
const { Connector, IpAddressTypes: IpAddressTypesValues } =
await import('@google-cloud/cloud-sql-connector');

// Create drizzle database instance
export const db = drizzle(pool, { schema });
const instanceConnectionName =
process.env.CLOUD_SQL_WAREHOUSE_CONNECTION_NAME;
const dbUser = process.env.WAREHOUSE_DB_USER;
const dbPassword = process.env.WAREHOUSE_DB_PASSWORD;
const dbName = process.env.WAREHOUSE_DB_NAME;

// Export for use in scripts
export const getPool = () => pool;
if (!instanceConnectionName || !dbUser || !dbName) {
throw new Error(
'Cloud SQL Connector requires CLOUD_SQL_WAREHOUSE_CONNECTION_NAME, WAREHOUSE_DB_USER, and WAREHOUSE_DB_NAME.'
);
}

const validTypes = Object.values(IpAddressTypesValues) as string[];
const ipAddressType = validTypes.includes(
process.env.CLOUD_SQL_WAREHOUSE_IP_TYPE ?? ''
)
? (process.env.CLOUD_SQL_WAREHOUSE_IP_TYPE as IpAddressTypes)
: IpAddressTypesValues.PUBLIC;

connector = new Connector();
const clientOpts = await connector.getOptions({
instanceConnectionName,
ipType: ipAddressType,
});

const newPool = new Pool({
...clientOpts,
user: dbUser,
password: dbPassword,
database: dbName,
max: 10,
});

newPool.on('error', (err) => {
console.error('Unexpected error on idle PostgreSQL client:', err);
});

console.log(
`PostgreSQL pool initialized via Cloud SQL Connector (${instanceConnectionName}).`
);
return newPool;
}

/**
* Creates a pool using a direct TCP connection via F3_DATA_WAREHOUSE_URL.
*/
function createDirectPool(): Pool {
const { F3_DATA_WAREHOUSE_URL } = loadEnvConfig();

const newPool = new Pool({
connectionString: F3_DATA_WAREHOUSE_URL,
});

newPool.on('error', (err) => {
console.error('Unexpected error on idle PostgreSQL client:', err);
});

return newPool;
}

/**
* Returns a cached Drizzle database instance for the F3 Data Warehouse.
*
* Connection mode is controlled by WAREHOUSE_DB_CONNECTION_MODE:
* "connector" → Cloud SQL Node.js Connector (authenticated, no public IP needed)
* "direct" → F3_DATA_WAREHOUSE_URL TCP connection (default)
*/
export async function getDb(): Promise<NodePgDatabase<typeof schema>> {
if (dbInstance) return dbInstance;

const mode = process.env.WAREHOUSE_DB_CONNECTION_MODE ?? 'direct';

if (mode === 'connector') {
pool = await createCloudSqlPool();
} else {
pool = createDirectPool();
}

dbInstance = drizzle(pool, { schema });
return dbInstance;
}

export async function getPool(): Promise<Pool> {
if (!pool) await getDb();
return pool!;
}

export async function close(): Promise<void> {
if (pool) {
await pool.end();
pool = null;
}
if (connector) {
connector.close();
connector = null;
}
dbInstance = null;
}
1 change: 1 addition & 0 deletions apps/region-pages/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
},
"dependencies": {
"dotenv": "^16.4.7",
"@google-cloud/cloud-sql-connector": "^1.4.1",
"lodash": "^4.17.21",
"next": "15.1.9",
"react": "^19.0.0",
Expand Down
3 changes: 2 additions & 1 deletion apps/region-pages/scripts/prune-regions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import {
regions as regionsSchema,
workouts as workoutsSchema,
} from '../drizzle/schema';
import { db as f3DataWarehouseDb } from '../drizzle/f3-data-warehouse/db';
import { getDb as getF3DataWarehouseDb } from '../drizzle/f3-data-warehouse/db';
import { orgs as orgsSchema } from '../drizzle/f3-data-warehouse/schema';

export async function pruneRegions() {
console.debug('🔄 pruning regions no longer in the warehouse...');

const f3DataWarehouseDb = await getF3DataWarehouseDb();
const activeWarehouseRegions = await f3DataWarehouseDb
.select({
id: orgsSchema.id,
Expand Down
3 changes: 2 additions & 1 deletion apps/region-pages/scripts/prune-workouts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import {
regions as regionsSchema,
workouts as workoutsSchema,
} from '../drizzle/schema';
import { db as f3DataWarehouseDb } from '../drizzle/f3-data-warehouse/db';
import { getDb as getF3DataWarehouseDb } from '../drizzle/f3-data-warehouse/db';
import { events as eventsSchema } from '../drizzle/f3-data-warehouse/schema';

export async function pruneWorkouts() {
console.debug('🔄 pruning workouts no longer in the warehouse...');

const f3DataWarehouseDb = await getF3DataWarehouseDb();
const activeWarehouseWorkouts = await f3DataWarehouseDb
.select({
id: eventsSchema.id,
Expand Down
3 changes: 2 additions & 1 deletion apps/region-pages/scripts/seed-regions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { kebabCase } from 'lodash';

import { db } from '../drizzle/db';
import { regions as regionsSchema } from '../drizzle/schema';
import { db as f3DataWarehouseDb } from '../drizzle/f3-data-warehouse/db';
import { getDb as getF3DataWarehouseDb } from '../drizzle/f3-data-warehouse/db';
import { orgs as orgsSchema } from '../drizzle/f3-data-warehouse/schema';
import { currentIngestedAt, isFresh } from './seed-state';

Expand Down Expand Up @@ -141,6 +141,7 @@ function transformInstagramUrl(instagram: string | null): string | null {
}

async function* fetchRegions(): AsyncGenerator<Region> {
const f3DataWarehouseDb = await getF3DataWarehouseDb();
const regions = await f3DataWarehouseDb
.select({
id: orgsSchema.id,
Expand Down
3 changes: 2 additions & 1 deletion apps/region-pages/scripts/seed-workouts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
regions as regionsSchema,
workouts as workoutsSchema,
} from '../drizzle/schema';
import { db as f3DataWarehouseDb } from '../drizzle/f3-data-warehouse/db';
import { getDb as getF3DataWarehouseDb } from '../drizzle/f3-data-warehouse/db';
import {
orgs as orgsSchema,
events as eventsSchema,
Expand Down Expand Up @@ -185,6 +185,7 @@ type FetchBatchArgs = {
};

async function fetchWorkoutsBatch(args: FetchBatchArgs): Promise<BatchResult> {
const f3DataWarehouseDb = await getF3DataWarehouseDb();
const conditions = [eq(eventsSchema.isActive, true)];
if (args.updatedAfter) {
conditions.push(gte(eventsSchema.updated, args.updatedAfter));
Expand Down
5 changes: 3 additions & 2 deletions apps/region-pages/src/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ export function loadEnvConfig() {
throw new Error(`POSTGRES_URL is not set in .env.${env}`);
}

if (!process.env.F3_DATA_WAREHOUSE_URL) {
const warehouseMode = process.env.WAREHOUSE_DB_CONNECTION_MODE ?? 'direct';
if (warehouseMode === 'direct' && !process.env.F3_DATA_WAREHOUSE_URL) {
throw new Error(`F3_DATA_WAREHOUSE_URL is not set in .env.${env}`);
}

return {
POSTGRES_URL: process.env.POSTGRES_URL,
F3_DATA_WAREHOUSE_URL: process.env.F3_DATA_WAREHOUSE_URL,
F3_DATA_WAREHOUSE_URL: process.env.F3_DATA_WAREHOUSE_URL ?? '',
NODE_ENV: env,
};
}
Loading
Loading