The progress collection was created before user_id was added to the schema, causing all user-keyed progress queries to return 400. Add an ensure_fields helper that idempotently patches existing collections with missing fields, and run it for progress.user_id on every init.
199 lines
6.9 KiB
Bash
Executable File
199 lines
6.9 KiB
Bash
Executable File
#!/bin/sh
|
|
# pb-init.sh — idempotent PocketBase collection bootstrap
|
|
#
|
|
# Creates all collections required by libnovel. Safe to re-run: POST returns
|
|
# 400/422 when a collection already exists; both are treated as success.
|
|
#
|
|
# Required env vars (with defaults):
|
|
# POCKETBASE_URL http://pocketbase:8090
|
|
# POCKETBASE_ADMIN_EMAIL admin@libnovel.local
|
|
# POCKETBASE_ADMIN_PASSWORD changeme123
|
|
|
|
set -e
|
|
|
|
PB_URL="${POCKETBASE_URL:-http://pocketbase:8090}"
|
|
PB_EMAIL="${POCKETBASE_ADMIN_EMAIL:-admin@libnovel.local}"
|
|
PB_PASSWORD="${POCKETBASE_ADMIN_PASSWORD:-changeme123}"
|
|
|
|
log() { echo "[pb-init] $*"; }
|
|
|
|
# ─── 1. Wait for PocketBase to be ready ──────────────────────────────────────
|
|
log "waiting for PocketBase at $PB_URL ..."
|
|
until wget -qO- "$PB_URL/api/health" > /dev/null 2>&1; do
|
|
sleep 2
|
|
done
|
|
log "PocketBase is up"
|
|
|
|
# ─── 2. Authenticate and obtain a superuser token ────────────────────────────
|
|
log "authenticating as $PB_EMAIL ..."
|
|
AUTH_RESPONSE=$(wget -qO- \
|
|
--header="Content-Type: application/json" \
|
|
--post-data="{\"identity\":\"$PB_EMAIL\",\"password\":\"$PB_PASSWORD\"}" \
|
|
"$PB_URL/api/collections/_superusers/auth-with-password")
|
|
|
|
TOKEN=$(echo "$AUTH_RESPONSE" | sed 's/.*"token":"\([^"]*\)".*/\1/')
|
|
if [ -z "$TOKEN" ] || [ "$TOKEN" = "$AUTH_RESPONSE" ]; then
|
|
log "ERROR: failed to obtain auth token. Response: $AUTH_RESPONSE"
|
|
exit 1
|
|
fi
|
|
log "auth token obtained"
|
|
|
|
# ─── 3. Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
create_collection() {
|
|
NAME="$1"
|
|
BODY="$2"
|
|
STATUS=$(wget -qSO- \
|
|
--header="Content-Type: application/json" \
|
|
--header="Authorization: Bearer $TOKEN" \
|
|
--post-data="$BODY" \
|
|
"$PB_URL/api/collections" 2>&1 | grep "HTTP/" | tail -1 | awk '{print $2}')
|
|
case "$STATUS" in
|
|
200|201) log "created collection: $NAME" ;;
|
|
400|422) log "collection already exists (skipped): $NAME" ;;
|
|
*) log "WARNING: unexpected status $STATUS for collection: $NAME" ;;
|
|
esac
|
|
}
|
|
|
|
# Patch a collection by name: adds any fields listed in FIELDS_JSON that are
|
|
# missing from the current schema. This is idempotent and safe to re-run.
|
|
ensure_fields() {
|
|
NAME="$1"
|
|
FIELDS_JSON="$2" # JSON array of field objects to guarantee exist
|
|
|
|
# Fetch current schema
|
|
CURRENT=$(wget -qO- \
|
|
--header="Authorization: Bearer $TOKEN" \
|
|
"$PB_URL/api/collections/$NAME" 2>/dev/null)
|
|
|
|
COLLECTION_ID=$(echo "$CURRENT" | sed 's/.*"id":"\([^"]*\)".*/\1/')
|
|
if [ -z "$COLLECTION_ID" ] || [ "$COLLECTION_ID" = "$CURRENT" ]; then
|
|
log "WARNING: could not get id for collection $NAME — skipping ensure_fields"
|
|
return
|
|
fi
|
|
|
|
# Build merged fields list via a small python snippet
|
|
MERGED=$(python3 - "$CURRENT" "$FIELDS_JSON" <<'PYEOF'
|
|
import sys, json
|
|
current = json.loads(sys.argv[1])
|
|
desired = json.loads(sys.argv[2])
|
|
existing_names = {f["name"] for f in current.get("fields", [])}
|
|
all_fields = list(current.get("fields", []))
|
|
added = []
|
|
for f in desired:
|
|
if f["name"] not in existing_names:
|
|
all_fields.append(f)
|
|
added.append(f["name"])
|
|
print(json.dumps({"fields": all_fields, "_added": added}))
|
|
PYEOF
|
|
)
|
|
|
|
ADDED=$(echo "$MERGED" | python3 -c 'import sys,json; d=json.load(sys.stdin); print(",".join(d.get("_added",[])))' 2>/dev/null)
|
|
if [ -z "$ADDED" ]; then
|
|
log "collection $NAME schema up-to-date"
|
|
return
|
|
fi
|
|
|
|
PATCH_BODY=$(echo "$MERGED" | python3 -c 'import sys,json; d=json.load(sys.stdin); d.pop("_added",None); print(json.dumps(d))' 2>/dev/null)
|
|
|
|
STATUS=$(wget -qSO- \
|
|
--header="Content-Type: application/json" \
|
|
--header="Authorization: Bearer $TOKEN" \
|
|
--body-data="$PATCH_BODY" \
|
|
--method=PATCH \
|
|
"$PB_URL/api/collections/$COLLECTION_ID" 2>&1 | grep "HTTP/" | tail -1 | awk '{print $2}')
|
|
case "$STATUS" in
|
|
200|201) log "patched collection $NAME — added fields: $ADDED" ;;
|
|
*) log "WARNING: patch returned $STATUS for collection $NAME" ;;
|
|
esac
|
|
}
|
|
|
|
# ─── 4. Create collections (idempotent — skips if already exist) ─────────────
|
|
|
|
create_collection "books" '{
|
|
"name": "books",
|
|
"type": "base",
|
|
"fields": [
|
|
{"name": "slug", "type": "text", "required": true},
|
|
{"name": "title", "type": "text", "required": true},
|
|
{"name": "author", "type": "text"},
|
|
{"name": "cover", "type": "text"},
|
|
{"name": "status", "type": "text"},
|
|
{"name": "genres", "type": "json"},
|
|
{"name": "summary", "type": "text"},
|
|
{"name": "total_chapters", "type": "number"},
|
|
{"name": "source_url", "type": "text"},
|
|
{"name": "ranking", "type": "number"},
|
|
{"name": "meta_updated", "type": "date"}
|
|
]
|
|
}'
|
|
|
|
create_collection "chapters_idx" '{
|
|
"name": "chapters_idx",
|
|
"type": "base",
|
|
"fields": [
|
|
{"name": "slug", "type": "text", "required": true},
|
|
{"name": "number", "type": "number", "required": true},
|
|
{"name": "title", "type": "text"},
|
|
{"name": "date_label", "type": "text"}
|
|
]
|
|
}'
|
|
|
|
create_collection "ranking" '{
|
|
"name": "ranking",
|
|
"type": "base",
|
|
"fields": [
|
|
{"name": "rank", "type": "number", "required": true},
|
|
{"name": "slug", "type": "text", "required": true},
|
|
{"name": "title", "type": "text"},
|
|
{"name": "author", "type": "text"},
|
|
{"name": "cover", "type": "text"},
|
|
{"name": "status", "type": "text"},
|
|
{"name": "genres", "type": "json"},
|
|
{"name": "source_url", "type": "text"},
|
|
{"name": "updated", "type": "date"}
|
|
]
|
|
}'
|
|
|
|
create_collection "progress" '{
|
|
"name": "progress",
|
|
"type": "base",
|
|
"fields": [
|
|
{"name": "session_id", "type": "text", "required": true},
|
|
{"name": "user_id", "type": "text"},
|
|
{"name": "slug", "type": "text", "required": true},
|
|
{"name": "chapter", "type": "number"},
|
|
{"name": "updated", "type": "date"}
|
|
]
|
|
}'
|
|
|
|
create_collection "audio_cache" '{
|
|
"name": "audio_cache",
|
|
"type": "base",
|
|
"fields": [
|
|
{"name": "cache_key", "type": "text", "required": true},
|
|
{"name": "filename", "type": "text"},
|
|
{"name": "updated", "type": "date"}
|
|
]
|
|
}'
|
|
|
|
create_collection "app_users" '{
|
|
"name": "app_users",
|
|
"type": "base",
|
|
"fields": [
|
|
{"name": "username", "type": "text", "required": true},
|
|
{"name": "password_hash", "type": "text", "required": true},
|
|
{"name": "role", "type": "text"},
|
|
{"name": "created", "type": "date"}
|
|
]
|
|
}'
|
|
|
|
# ─── 5. Schema migrations (idempotent field additions) ───────────────────────
|
|
# Ensures fields added after initial deploy are present in existing instances.
|
|
|
|
ensure_fields "progress" '[
|
|
{"name": "user_id", "type": "text"}
|
|
]'
|
|
|
|
log "all collections ready"
|