app.bettersight.io/backend/pb_migrations/008_add_trip_watchlist.js

63 lines
2.5 KiB
JavaScript

/// <reference path="../pb_data/types.d.ts" />
//
// Adds `trip_watchlist` — the per-seat trip-monitoring feature. Each
// active seat can pin a small, tier-capped number of specific already-
// analyzed competitor trips (WATCHLIST_CAP_PER_SEAT in
// services/watchlist_service.py — 1 per seat at Analyse). The nightly
// n8n cron re-scrapes exactly these URLs instead of each competitor's
// root website, replacing an unbounded, low-signal daily scrape (a
// homepage `is_generic_homepage()` already flags as non-trip-specific)
// with a small, predictable, and meaningful one — every resulting
// price/product alert is about a trip a PM explicitly chose to track.
//
// Reuses the exact same confirmed_url / per-url content-hash caching
// already built for manual trip-intent analysis this session — no
// schema change needed on products/scrape_runs/competitors, and no
// jobs.py/analysis_service.py code change either. See
// n8n/workflows/daily_scrape_cron.json for the cron-side wiring.
migrate((app) => {
const tenants = app.findCollectionByNameOrId('tenants');
const tenantSeats = app.findCollectionByNameOrId('tenant_seats');
const competitors = app.findCollectionByNameOrId('competitors');
const rel = (name, collection, opts = {}) => ({
name, type: 'relation', collectionId: collection.id,
cascadeDelete: false, minSelect: 0, maxSelect: 1, ...opts,
});
const text = (name, opts = {}) => ({ name, type: 'text', ...opts });
const autodate = (name, onCreate, onUpdate = false) => ({
name, type: 'autodate', onCreate, onUpdate,
});
const collection = new Collection({
type: 'base',
name: 'trip_watchlist',
fields: [
rel('tenant_id', tenants, { required: true }),
rel('seat_id', tenantSeats, { required: true }),
rel('competitor_id', competitors, { required: true }),
text('url', { required: true }),
text('trip_name'),
autodate('added_at', true),
],
indexes: [
'CREATE INDEX idx_trip_watchlist_seat ON trip_watchlist (seat_id)',
'CREATE INDEX idx_trip_watchlist_tenant ON trip_watchlist (tenant_id)',
],
// Same access model as every other tenant-scoped collection — Flask
// is the only direct PocketBase client, enforcing tenant_id/seat_id
// scoping itself.
listRule: null,
viewRule: null,
createRule: null,
updateRule: null,
deleteRule: null,
});
return app.save(collection);
}, (app) => {
const collection = app.findCollectionByNameOrId('trip_watchlist');
if (collection) app.delete(collection);
});