diff --git a/.env.example b/.env.example index f5565e26..909a6678 100644 --- a/.env.example +++ b/.env.example @@ -106,6 +106,7 @@ REPRESENT_API_URL=https://represent.opennorth.ca # --- NocoDB v2 (read-only data browser) --- # NocoDB uses its own database (nocodb_meta) to avoid conflicts with Prisma # The database is auto-created by init-nocodb-db.sh on first PostgreSQL startup +# nocodb-init container auto-registers changemaker_v2 as a browsable data source NOCODB_V2_PORT=8091 NOCODB_URL=http://changemaker-v2-nocodb:8080 NOCODB_PORT=8091 diff --git a/docker-compose.yml b/docker-compose.yml index fdc0bc90..e67a8263 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -290,6 +290,31 @@ services: networks: - changemaker-lite + # NocoDB Init — auto-registers changemaker_v2 as a browsable data source + nocodb-init: + image: alpine:3 + container_name: nocodb-init + depends_on: + nocodb-v2: + condition: service_healthy + v2-postgres: + condition: service_healthy + restart: "no" + environment: + NOCODB_URL: http://changemaker-v2-nocodb:8080 + NC_ADMIN_EMAIL: ${NC_ADMIN_EMAIL:-admin@cmlite.org} + NC_ADMIN_PASSWORD: ${NC_ADMIN_PASSWORD:?NC_ADMIN_PASSWORD must be set in .env} + DB_HOST: changemaker-v2-postgres + DB_PORT: "5432" + DB_USER: ${V2_POSTGRES_USER:-changemaker} + DB_PASSWORD: ${V2_POSTGRES_PASSWORD:-changemaker} + DB_NAME: ${V2_POSTGRES_DB:-changemaker_v2} + volumes: + - ./scripts/nocodb-init.sh:/init.sh:ro + entrypoint: ["/bin/sh", "/init.sh"] + networks: + - changemaker-lite + # ========================================================================= # SHARED INFRASTRUCTURE (kept from v1) # ========================================================================= diff --git a/scripts/nocodb-init.sh b/scripts/nocodb-init.sh new file mode 100755 index 00000000..fc8022bf --- /dev/null +++ b/scripts/nocodb-init.sh @@ -0,0 +1,209 @@ +#!/bin/sh +set -e + +PREFIX="[nocodb-init]" + +# ─── Utilities ──────────────────────────────────────────────────────────────── + +log() { echo "$PREFIX $1"; } +fail() { echo "$PREFIX ERROR: $1" >&2; exit 1; } + +api() { + # api METHOD /path [data] + METHOD="$1"; ENDPOINT="$2"; DATA="$3" + if [ -n "$DATA" ]; then + curl -sf -X "$METHOD" "${NOCODB_URL}${ENDPOINT}" \ + -H "Content-Type: application/json" \ + -H "xc-auth: ${TOKEN}" \ + -d "$DATA" + else + curl -sf -X "$METHOD" "${NOCODB_URL}${ENDPOINT}" \ + -H "Content-Type: application/json" \ + -H "xc-auth: ${TOKEN}" + fi +} + +# ─── Install dependencies ───────────────────────────────────────────────────── + +apk add --no-cache curl jq >/dev/null 2>&1 +log "Dependencies installed" + +# ─── Step 1: Authenticate ───────────────────────────────────────────────────── + +log "Authenticating with NocoDB..." +TOKEN="" +for i in $(seq 1 15); do + RESPONSE=$(curl -sf -X POST "${NOCODB_URL}/api/v1/auth/user/signin" \ + -H "Content-Type: application/json" \ + -d "{\"email\":\"${NC_ADMIN_EMAIL}\",\"password\":\"${NC_ADMIN_PASSWORD}\"}" 2>/dev/null) || true + + if [ -n "$RESPONSE" ]; then + TOKEN=$(echo "$RESPONSE" | jq -r '.token // empty') + if [ -n "$TOKEN" ]; then + break + fi + fi + + log " Waiting for NocoDB to be ready... (attempt $i/15)" + sleep 3 +done + +if [ -z "$TOKEN" ]; then + fail "Could not authenticate with NocoDB after 15 attempts" +fi +log "Authenticated" + +# ─── Step 2: Check for existing base ────────────────────────────────────────── + +BASE_NAME="Changemaker Platform" +INTEGRATION_NAME="Changemaker PostgreSQL" + +log "Checking for existing base..." +BASES=$(api GET "/api/v2/meta/bases/") +EXISTING_BASE_ID=$(echo "$BASES" | jq -r --arg name "$BASE_NAME" '.list[] | select(.title == $name) | .id // empty' 2>/dev/null) + +if [ -n "$EXISTING_BASE_ID" ]; then + # Base exists — check if it has tables (fully synced) + TABLES=$(api GET "/api/v2/meta/bases/${EXISTING_BASE_ID}/tables") + TABLE_COUNT=$(echo "$TABLES" | jq -r '.list | length' 2>/dev/null) + + if [ "$TABLE_COUNT" -gt 0 ] 2>/dev/null; then + log "Base '${BASE_NAME}' already exists with ${TABLE_COUNT} tables. Nothing to do." + exit 0 + fi + + # Check if an integration-linked source already exists (sync may still be in progress) + # NocoDB auto-creates a default source (fk_integration_id=null, is_local=true) — ignore it + SOURCES=$(api GET "/api/v2/meta/bases/${EXISTING_BASE_ID}/sources" 2>/dev/null) || true + HAS_REAL_SOURCE=$(echo "$SOURCES" | jq -r '[.list[] | select(.fk_integration_id != null)] | length' 2>/dev/null) + + if [ "$HAS_REAL_SOURCE" -gt 0 ] 2>/dev/null; then + log "Base has an integration source — table sync in progress, waiting..." + for i in $(seq 1 30); do + TABLES=$(api GET "/api/v2/meta/bases/${EXISTING_BASE_ID}/tables" 2>/dev/null) || true + TABLE_COUNT=$(echo "$TABLES" | jq -r '.list | length' 2>/dev/null) + if [ "$TABLE_COUNT" -gt 0 ] 2>/dev/null; then + log "Table sync complete: ${TABLE_COUNT} tables discovered" + exit 0 + fi + sleep 3 + done + log "Source exists, tables will appear eventually. Nothing more to do." + exit 0 + fi + + log "Base exists but has no integration source — will add one" +fi + +# ─── Step 3: Create or reuse integration ────────────────────────────────────── + +log "Checking for existing integration..." +INTEGRATIONS=$(api GET "/api/v2/meta/integrations") +INTEGRATION_ID=$(echo "$INTEGRATIONS" | jq -r --arg name "$INTEGRATION_NAME" '.list[] | select(.title == $name) | .id // empty' 2>/dev/null) + +if [ -z "$INTEGRATION_ID" ]; then + log "Creating integration '${INTEGRATION_NAME}'..." + INTEGRATION_PAYLOAD=$(cat </dev/null +log "Source added — table discovery started" + +# ─── Step 5: Wait for table sync ───────────────────────────────────────────── + +log "Waiting for table discovery..." +for i in $(seq 1 30); do + TABLES=$(api GET "/api/v2/meta/bases/${BASE_ID}/tables" 2>/dev/null) || true + TABLE_COUNT=$(echo "$TABLES" | jq -r '.list | length' 2>/dev/null) + + if [ "$TABLE_COUNT" -gt 0 ] 2>/dev/null; then + log "Table sync complete: ${TABLE_COUNT} tables discovered" + break + fi + + if [ "$i" = "30" ]; then + log "Table sync still in progress after 90s — tables will appear eventually" + break + fi + + sleep 3 +done + +# ─── Step 6: Clean up default empty base ────────────────────────────────────── + +log "Checking for default empty base..." +# Re-fetch bases list (may have changed since step 2) +BASES=$(api GET "/api/v2/meta/bases/") +DEFAULT_BASE_ID=$(echo "$BASES" | jq -r '.list[] | select(.title == "Base") | .id // empty' 2>/dev/null) + +if [ -n "$DEFAULT_BASE_ID" ] && [ "$DEFAULT_BASE_ID" != "$BASE_ID" ]; then + log "Deleting default empty base..." + api DELETE "/api/v2/meta/bases/${DEFAULT_BASE_ID}" >/dev/null 2>&1 || true + log "Default base cleaned up" +fi + +log "Done"