feat: Initialize BizMatch application with core UI components, routing, listing pages, backend services, migration scripts, and vulnerability management.
This commit is contained in:
@@ -42,15 +42,14 @@
|
||||
"cls-hooked": "^4.2.2",
|
||||
"cors": "^2.8.5",
|
||||
"drizzle-orm": "^0.32.0",
|
||||
"firebase": "^11.3.1",
|
||||
"firebase": "^11.9.0",
|
||||
"firebase-admin": "^13.1.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"groq-sdk": "^0.5.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"nest-winston": "^1.9.4",
|
||||
"nestjs-cls": "^5.4.0",
|
||||
"nodemailer": "^6.9.10",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"nodemailer": "^7.0.12",
|
||||
"openai": "^4.52.6",
|
||||
"pg": "^8.11.5",
|
||||
"pgvector": "^0.2.0",
|
||||
@@ -75,7 +74,7 @@
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/pg": "^8.11.5",
|
||||
"commander": "^12.0.0",
|
||||
"drizzle-kit": "^0.23.2",
|
||||
"drizzle-kit": "^0.31.8",
|
||||
"esbuild-register": "^3.5.0",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
|
||||
117
bizmatch-server/scripts/migrate-slugs.sql
Normal file
117
bizmatch-server/scripts/migrate-slugs.sql
Normal file
@@ -0,0 +1,117 @@
|
||||
-- =============================================================
|
||||
-- SEO SLUG MIGRATION SCRIPT
|
||||
-- Run this directly in your PostgreSQL database
|
||||
-- =============================================================
|
||||
|
||||
-- First, let's see how many listings need slugs
|
||||
SELECT 'Businesses without slugs: ' || COUNT(*) FROM businesses_json
|
||||
WHERE data->>'slug' IS NULL OR data->>'slug' = '';
|
||||
|
||||
SELECT 'Commercial properties without slugs: ' || COUNT(*) FROM commercials_json
|
||||
WHERE data->>'slug' IS NULL OR data->>'slug' = '';
|
||||
|
||||
-- =============================================================
|
||||
-- UPDATE BUSINESS LISTINGS WITH SEO SLUGS
|
||||
-- Format: title-city-state-shortid (e.g., restaurant-austin-tx-a3f7b2c1)
|
||||
-- =============================================================
|
||||
|
||||
UPDATE businesses_json
|
||||
SET data = jsonb_set(
|
||||
data::jsonb,
|
||||
'{slug}',
|
||||
to_jsonb(
|
||||
LOWER(
|
||||
REGEXP_REPLACE(
|
||||
REGEXP_REPLACE(
|
||||
CONCAT(
|
||||
-- Title (first 50 chars, cleaned)
|
||||
SUBSTRING(
|
||||
REGEXP_REPLACE(
|
||||
LOWER(COALESCE(data->>'title', '')),
|
||||
'[^a-z0-9\s-]', '', 'g'
|
||||
), 1, 50
|
||||
),
|
||||
'-',
|
||||
-- City or County
|
||||
REGEXP_REPLACE(
|
||||
LOWER(COALESCE(data->'location'->>'name', data->'location'->>'county', '')),
|
||||
'[^a-z0-9\s-]', '', 'g'
|
||||
),
|
||||
'-',
|
||||
-- State
|
||||
LOWER(COALESCE(data->'location'->>'state', '')),
|
||||
'-',
|
||||
-- First 8 chars of UUID
|
||||
SUBSTRING(id::text, 1, 8)
|
||||
),
|
||||
'\s+', '-', 'g' -- Replace spaces with hyphens
|
||||
),
|
||||
'-+', '-', 'g' -- Replace multiple hyphens with single
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
WHERE data->>'slug' IS NULL OR data->>'slug' = '';
|
||||
|
||||
-- =============================================================
|
||||
-- UPDATE COMMERCIAL PROPERTIES WITH SEO SLUGS
|
||||
-- =============================================================
|
||||
|
||||
UPDATE commercials_json
|
||||
SET data = jsonb_set(
|
||||
data::jsonb,
|
||||
'{slug}',
|
||||
to_jsonb(
|
||||
LOWER(
|
||||
REGEXP_REPLACE(
|
||||
REGEXP_REPLACE(
|
||||
CONCAT(
|
||||
-- Title (first 50 chars, cleaned)
|
||||
SUBSTRING(
|
||||
REGEXP_REPLACE(
|
||||
LOWER(COALESCE(data->>'title', '')),
|
||||
'[^a-z0-9\s-]', '', 'g'
|
||||
), 1, 50
|
||||
),
|
||||
'-',
|
||||
-- City or County
|
||||
REGEXP_REPLACE(
|
||||
LOWER(COALESCE(data->'location'->>'name', data->'location'->>'county', '')),
|
||||
'[^a-z0-9\s-]', '', 'g'
|
||||
),
|
||||
'-',
|
||||
-- State
|
||||
LOWER(COALESCE(data->'location'->>'state', '')),
|
||||
'-',
|
||||
-- First 8 chars of UUID
|
||||
SUBSTRING(id::text, 1, 8)
|
||||
),
|
||||
'\s+', '-', 'g' -- Replace spaces with hyphens
|
||||
),
|
||||
'-+', '-', 'g' -- Replace multiple hyphens with single
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
WHERE data->>'slug' IS NULL OR data->>'slug' = '';
|
||||
|
||||
-- =============================================================
|
||||
-- VERIFY THE RESULTS
|
||||
-- =============================================================
|
||||
|
||||
SELECT 'Migration complete! Checking results...' AS status;
|
||||
|
||||
-- Show sample of updated slugs
|
||||
SELECT
|
||||
id,
|
||||
data->>'title' AS title,
|
||||
data->>'slug' AS slug
|
||||
FROM businesses_json
|
||||
LIMIT 5;
|
||||
|
||||
SELECT
|
||||
id,
|
||||
data->>'title' AS title,
|
||||
data->>'slug' AS slug
|
||||
FROM commercials_json
|
||||
LIMIT 5;
|
||||
162
bizmatch-server/scripts/migrate-slugs.ts
Normal file
162
bizmatch-server/scripts/migrate-slugs.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Migration Script: Generate Slugs for Existing Listings
|
||||
*
|
||||
* This script generates SEO-friendly slugs for all existing businesses
|
||||
* and commercial properties that don't have slugs yet.
|
||||
*
|
||||
* Run with: npx ts-node scripts/migrate-slugs.ts
|
||||
*/
|
||||
|
||||
import { Pool } from 'pg';
|
||||
import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { sql, eq, isNull } from 'drizzle-orm';
|
||||
import * as schema from '../src/drizzle/schema';
|
||||
|
||||
// Slug generation function (copied from utils for standalone execution)
|
||||
function generateSlug(title: string, location: any, id: string): string {
|
||||
if (!title || !id) return id; // Fallback to ID if no title
|
||||
|
||||
const titleSlug = title
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.substring(0, 50);
|
||||
|
||||
let locationSlug = '';
|
||||
if (location) {
|
||||
const locationName = location.name || location.county || '';
|
||||
const state = location.state || '';
|
||||
|
||||
if (locationName) {
|
||||
locationSlug = locationName
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-');
|
||||
}
|
||||
|
||||
if (state) {
|
||||
locationSlug = locationSlug
|
||||
? `${locationSlug}-${state.toLowerCase()}`
|
||||
: state.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
const shortId = id.substring(0, 8);
|
||||
const parts = [titleSlug, locationSlug, shortId].filter(Boolean);
|
||||
return parts.join('-').replace(/-+/g, '-').replace(/^-|-$/g, '').toLowerCase();
|
||||
}
|
||||
|
||||
async function migrateBusinessSlugs(db: NodePgDatabase<typeof schema>) {
|
||||
console.log('🔄 Migrating Business Listings...');
|
||||
|
||||
// Get all businesses without slugs
|
||||
const businesses = await db
|
||||
.select({
|
||||
id: schema.businesses_json.id,
|
||||
email: schema.businesses_json.email,
|
||||
data: schema.businesses_json.data,
|
||||
})
|
||||
.from(schema.businesses_json);
|
||||
|
||||
let updated = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const business of businesses) {
|
||||
const data = business.data as any;
|
||||
|
||||
// Skip if slug already exists
|
||||
if (data.slug) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const slug = generateSlug(data.title || '', data.location || {}, business.id);
|
||||
|
||||
// Update with new slug
|
||||
const updatedData = { ...data, slug };
|
||||
await db
|
||||
.update(schema.businesses_json)
|
||||
.set({ data: updatedData })
|
||||
.where(eq(schema.businesses_json.id, business.id));
|
||||
|
||||
console.log(` ✓ ${data.title?.substring(0, 40)}... → ${slug}`);
|
||||
updated++;
|
||||
}
|
||||
|
||||
console.log(`✅ Business Listings: ${updated} updated, ${skipped} skipped (already had slugs)`);
|
||||
return updated;
|
||||
}
|
||||
|
||||
async function migrateCommercialSlugs(db: NodePgDatabase<typeof schema>) {
|
||||
console.log('\n🔄 Migrating Commercial Properties...');
|
||||
|
||||
// Get all commercial properties without slugs
|
||||
const properties = await db
|
||||
.select({
|
||||
id: schema.commercials_json.id,
|
||||
email: schema.commercials_json.email,
|
||||
data: schema.commercials_json.data,
|
||||
})
|
||||
.from(schema.commercials_json);
|
||||
|
||||
let updated = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const property of properties) {
|
||||
const data = property.data as any;
|
||||
|
||||
// Skip if slug already exists
|
||||
if (data.slug) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const slug = generateSlug(data.title || '', data.location || {}, property.id);
|
||||
|
||||
// Update with new slug
|
||||
const updatedData = { ...data, slug };
|
||||
await db
|
||||
.update(schema.commercials_json)
|
||||
.set({ data: updatedData })
|
||||
.where(eq(schema.commercials_json.id, property.id));
|
||||
|
||||
console.log(` ✓ ${data.title?.substring(0, 40)}... → ${slug}`);
|
||||
updated++;
|
||||
}
|
||||
|
||||
console.log(`✅ Commercial Properties: ${updated} updated, ${skipped} skipped (already had slugs)`);
|
||||
return updated;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('═══════════════════════════════════════════════════════');
|
||||
console.log(' SEO SLUG MIGRATION SCRIPT');
|
||||
console.log('═══════════════════════════════════════════════════════\n');
|
||||
|
||||
// Connect to database
|
||||
const connectionString = process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/bizmatch';
|
||||
console.log(`📡 Connecting to database...`);
|
||||
|
||||
const pool = new Pool({ connectionString });
|
||||
const db = drizzle(pool, { schema });
|
||||
|
||||
try {
|
||||
const businessCount = await migrateBusinessSlugs(db);
|
||||
const commercialCount = await migrateCommercialSlugs(db);
|
||||
|
||||
console.log('\n═══════════════════════════════════════════════════════');
|
||||
console.log(`🎉 Migration complete! Total: ${businessCount + commercialCount} listings updated`);
|
||||
console.log('═══════════════════════════════════════════════════════\n');
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -12,6 +12,9 @@ async function bootstrap() {
|
||||
const logger = app.get<LoggerService>(WINSTON_MODULE_NEST_PROVIDER);
|
||||
app.useLogger(logger);
|
||||
//app.use('/bizmatch/payment/webhook', bodyParser.raw({ type: 'application/json' }));
|
||||
// Serve static files from pictures directory
|
||||
app.use('/pictures', express.static('pictures'));
|
||||
|
||||
app.setGlobalPrefix('bizmatch');
|
||||
|
||||
app.enableCors({
|
||||
|
||||
Reference in New Issue
Block a user