a1ba5ee093
Replace Supabase auth and search runtime with a local Fastify API, PostgreSQL/PostGIS schema, and local session handling. Scaffold the worker and deep-research foundations while keeping the existing research, dashboard, and map flows running on the new backend.
232 lines
7.8 KiB
TypeScript
232 lines
7.8 KiB
TypeScript
import type { Pool, PoolClient } from 'pg';
|
|
import type { BusinessUpsertRecord } from './google-places.js';
|
|
import { mapBusinessRow, mapSearchJobRow, type BusinessDto, type BusinessRow, type RunSearchInput, type SearchJobDto, type SearchJobResultLinkDto, type SearchJobRow, } from './types.js';
|
|
|
|
type DbClient = Pool | PoolClient;
|
|
|
|
export async function createSearchJob(db: DbClient, userId: string, payload: RunSearchInput) {
|
|
const now = new Date().toISOString();
|
|
const jobName = payload.name || `${payload.businessType} in ${payload.location}`;
|
|
|
|
const result = await db.query<SearchJobRow>(
|
|
`
|
|
insert into public.search_jobs (
|
|
user_id, name, city, radius_km, business_type, keywords, status, total_results, started_at, created_at, updated_at
|
|
)
|
|
values ($1, $2, $3, $4, $5, $6, 'running', 0, $7, $7, $7)
|
|
returning id, user_id, name, city, address, postal_code, radius_km, business_type, keywords,
|
|
status, total_results, started_at, completed_at, created_at, updated_at
|
|
`,
|
|
[userId, jobName, payload.location, payload.radiusKm, payload.businessType, payload.keywords ?? null, now],
|
|
);
|
|
|
|
return mapSearchJobRow(result.rows[0]);
|
|
}
|
|
|
|
export async function updateSearchJobCenter(db: DbClient, jobId: string, lat: number, lng: number) {
|
|
await db.query(
|
|
`
|
|
update public.search_jobs
|
|
set search_center_geom = ST_SetSRID(ST_MakePoint($2, $1), 4326)::geography,
|
|
updated_at = now()
|
|
where id = $3
|
|
`,
|
|
[lat, lng, jobId],
|
|
);
|
|
}
|
|
|
|
export async function completeSearchJob(db: DbClient, jobId: string, totalResults: number) {
|
|
const completedAt = new Date().toISOString();
|
|
const result = await db.query<SearchJobRow>(
|
|
`
|
|
update public.search_jobs
|
|
set total_results = $2,
|
|
status = 'completed',
|
|
completed_at = $3,
|
|
updated_at = $3
|
|
where id = $1
|
|
returning id, user_id, name, city, address, postal_code, radius_km, business_type, keywords,
|
|
status, total_results, started_at, completed_at, created_at, updated_at
|
|
`,
|
|
[jobId, totalResults, completedAt],
|
|
);
|
|
|
|
return mapSearchJobRow(result.rows[0]);
|
|
}
|
|
|
|
export async function failSearchJob(db: DbClient, jobId: string) {
|
|
await db.query(
|
|
`
|
|
update public.search_jobs
|
|
set status = 'failed', updated_at = now()
|
|
where id = $1
|
|
`,
|
|
[jobId],
|
|
);
|
|
}
|
|
|
|
export async function upsertBusiness(db: DbClient, userId: string, business: BusinessUpsertRecord) {
|
|
const result = await db.query<{ id: string }>(
|
|
`
|
|
insert into public.businesses (
|
|
user_id, external_source_id, source, name, address, city, state_province, postal_code, country,
|
|
phone, website, rating, review_count, category, latitude, longitude, geom, metadata_json,
|
|
first_seen_at, last_seen_at, updated_at
|
|
)
|
|
values (
|
|
$1, $2, $3, $4, $5, $6, $7, $8, $9,
|
|
$10, $11, $12, $13, $14, $15, $16,
|
|
case when $15 is not null and $16 is not null then ST_SetSRID(ST_MakePoint($16, $15), 4326)::geography else null end,
|
|
$17::jsonb, $18, $19, $20
|
|
)
|
|
on conflict (user_id, source, external_source_id)
|
|
do update set
|
|
name = excluded.name,
|
|
address = excluded.address,
|
|
city = excluded.city,
|
|
state_province = excluded.state_province,
|
|
postal_code = excluded.postal_code,
|
|
country = excluded.country,
|
|
phone = excluded.phone,
|
|
website = excluded.website,
|
|
rating = excluded.rating,
|
|
review_count = excluded.review_count,
|
|
category = excluded.category,
|
|
latitude = excluded.latitude,
|
|
longitude = excluded.longitude,
|
|
geom = excluded.geom,
|
|
metadata_json = excluded.metadata_json,
|
|
last_seen_at = excluded.last_seen_at,
|
|
updated_at = excluded.updated_at
|
|
returning id
|
|
`,
|
|
[
|
|
userId,
|
|
business.externalSourceId,
|
|
business.source,
|
|
business.name,
|
|
business.address,
|
|
business.city,
|
|
business.stateProvince,
|
|
business.postalCode,
|
|
business.country,
|
|
business.phone,
|
|
business.website,
|
|
business.rating,
|
|
business.reviewCount,
|
|
business.category,
|
|
business.latitude,
|
|
business.longitude,
|
|
JSON.stringify(business.metadataJson),
|
|
business.firstSeenAt,
|
|
business.lastSeenAt,
|
|
business.updatedAt,
|
|
],
|
|
);
|
|
|
|
return result.rows[0].id;
|
|
}
|
|
|
|
export async function upsertSearchJobResult(
|
|
db: DbClient,
|
|
input: { userId: string; searchJobId: string; businessId: string; matchedKeywords: string[] | null; rank: number; capturedAt: string },
|
|
) {
|
|
await db.query(
|
|
`
|
|
insert into public.search_job_results (user_id, search_job_id, business_id, matched_keywords, rank, captured_at)
|
|
values ($1, $2, $3, $4, $5, $6)
|
|
on conflict (search_job_id, business_id)
|
|
do update set
|
|
matched_keywords = excluded.matched_keywords,
|
|
rank = excluded.rank,
|
|
captured_at = excluded.captured_at
|
|
`,
|
|
[input.userId, input.searchJobId, input.businessId, input.matchedKeywords, input.rank, input.capturedAt],
|
|
);
|
|
}
|
|
|
|
export async function listSearchJobsForUser(db: DbClient, userId: string, limit = 100) {
|
|
const result = await db.query<SearchJobRow>(
|
|
`
|
|
select id, user_id, name, city, address, postal_code, radius_km, business_type, keywords,
|
|
status, total_results, started_at, completed_at, created_at, updated_at
|
|
from public.search_jobs
|
|
where user_id = $1
|
|
order by created_at desc
|
|
limit $2
|
|
`,
|
|
[userId, limit],
|
|
);
|
|
|
|
return result.rows.map(mapSearchJobRow);
|
|
}
|
|
|
|
export async function getSearchJobForUser(db: DbClient, userId: string, jobId: string) {
|
|
const result = await db.query<SearchJobRow>(
|
|
`
|
|
select id, user_id, name, city, address, postal_code, radius_km, business_type, keywords,
|
|
status, total_results, started_at, completed_at, created_at, updated_at
|
|
from public.search_jobs
|
|
where user_id = $1 and id = $2
|
|
limit 1
|
|
`,
|
|
[userId, jobId],
|
|
);
|
|
|
|
if (result.rowCount === 0) {
|
|
return null;
|
|
}
|
|
|
|
return mapSearchJobRow(result.rows[0]);
|
|
}
|
|
|
|
export async function listBusinessesForUser(db: DbClient, userId: string) {
|
|
const result = await db.query<BusinessRow>(
|
|
`
|
|
select id, user_id, external_source_id, source, name, address, city, state_province, postal_code,
|
|
country, phone, website, rating, review_count, category, hours_json, latitude, longitude,
|
|
general_info, metadata_json, first_seen_at, last_seen_at, created_at, updated_at
|
|
from public.businesses
|
|
where user_id = $1
|
|
order by created_at desc
|
|
`,
|
|
[userId],
|
|
);
|
|
|
|
return result.rows.map(mapBusinessRow);
|
|
}
|
|
|
|
export async function listSearchJobResultLinksForUser(db: DbClient, userId: string): Promise<SearchJobResultLinkDto[]> {
|
|
const result = await db.query<{ business_id: string; search_job_id: string }>(
|
|
`
|
|
select business_id, search_job_id
|
|
from public.search_job_results
|
|
where user_id = $1
|
|
`,
|
|
[userId],
|
|
);
|
|
|
|
return result.rows.map((row) => ({ businessId: row.business_id, searchJobId: row.search_job_id }));
|
|
}
|
|
|
|
export async function listBusinessesForJobIds(db: DbClient, userId: string, jobIds: string[]): Promise<BusinessDto[]> {
|
|
if (jobIds.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const result = await db.query<BusinessRow>(
|
|
`
|
|
select distinct b.id, b.user_id, b.external_source_id, b.source, b.name, b.address, b.city, b.state_province, b.postal_code,
|
|
b.country, b.phone, b.website, b.rating, b.review_count, b.category, b.hours_json, b.latitude, b.longitude,
|
|
b.general_info, b.metadata_json, b.first_seen_at, b.last_seen_at, b.created_at, b.updated_at
|
|
from public.businesses b
|
|
join public.search_job_results r on r.business_id = b.id
|
|
where b.user_id = $1 and r.search_job_id = any($2::uuid[])
|
|
order by b.created_at desc
|
|
`,
|
|
[userId, jobIds],
|
|
);
|
|
|
|
return result.rows.map(mapBusinessRow);
|
|
}
|