feat(scripts): helcim plan consolidation migration (dry-run default)
This commit is contained in:
parent
fb337a4277
commit
daea8b65be
3 changed files with 433 additions and 0 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -26,6 +26,9 @@ logs
|
||||||
!.env.example
|
!.env.example
|
||||||
scripts/*.js
|
scripts/*.js
|
||||||
|
|
||||||
|
# Migration backup files
|
||||||
|
.migration-backup-*.json
|
||||||
|
|
||||||
# Playwright
|
# Playwright
|
||||||
e2e/test-results/
|
e2e/test-results/
|
||||||
playwright-report/
|
playwright-report/
|
||||||
|
|
|
||||||
51
scripts/README.md
Normal file
51
scripts/README.md
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# scripts/
|
||||||
|
|
||||||
|
One-off admin and migration scripts. None of these run in CI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## helcim-plan-consolidation.js
|
||||||
|
|
||||||
|
Consolidates Helcim payment plans from the legacy per-tier model (multiple plans at $15/$30/$50) to two unified plans (Monthly Membership at $15, Annual Membership at $150), and backfills Mongo members.
|
||||||
|
|
||||||
|
### Required env
|
||||||
|
|
||||||
|
```
|
||||||
|
HELCIM_API_TOKEN=<token>
|
||||||
|
MONGODB_URI=<mongodb connection string>
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy `.env.example` → `.env` if you haven't already.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dry-run (default) — no mutations, writes backup file
|
||||||
|
node scripts/helcim-plan-consolidation.js
|
||||||
|
|
||||||
|
# Execute all steps: delete subs, delete legacy plans, create new plans, update Mongo
|
||||||
|
node scripts/helcim-plan-consolidation.js --confirm
|
||||||
|
|
||||||
|
# Mongo cleanup only — skips all Helcim steps (use if Helcim steps already ran)
|
||||||
|
node scripts/helcim-plan-consolidation.js --mongo-only
|
||||||
|
```
|
||||||
|
|
||||||
|
### What it does
|
||||||
|
|
||||||
|
1. **Backup** — always written to `.migration-backup-<timestamp>.json`, even in dry-run. Contains all Helcim subscriptions, payment plans, and Mongo members with `helcimSubscriptionId`.
|
||||||
|
2. **Delete subscriptions** — deletes all Helcim subscriptions (`--confirm` only).
|
||||||
|
3. **Delete legacy plans** — deletes plan IDs `20162`, `21596`, `21597`, `21598`, plus any plan whose name matches the legacy pattern (e.g. "Ghost Guild - Member ($15)").
|
||||||
|
4. **Create Monthly Membership** — $15/month. Idempotent: skipped if a plan named "Monthly Membership" already exists.
|
||||||
|
5. **Create Annual Membership** — $150/year. Same idempotency. Uses `billingPeriod: "yearly"` (Helcim v2 convention); if Helcim returns 4xx, the script aborts and prints the full error so you can correct the field name.
|
||||||
|
6. **Mongo cleanup** — `updateMany` with `runValidators: false` on all members with `helcimSubscriptionId`: sets `contributionTier: '0'` and `billingCadence: 'monthly'`, unsets `helcimSubscriptionId`. Does **not** change `status`.
|
||||||
|
|
||||||
|
At the end, prints new plan IDs formatted for copy-paste into `.env`.
|
||||||
|
|
||||||
|
### Recovery
|
||||||
|
|
||||||
|
If something goes wrong mid-run, the backup JSON contains the pre-migration state. You can use the backup to:
|
||||||
|
- Identify which subscriptions existed before deletion.
|
||||||
|
- Identify which plan IDs to recreate manually in Helcim.
|
||||||
|
- Identify which members had `helcimSubscriptionId` before it was unset.
|
||||||
|
|
||||||
|
Backup files are gitignored (`.migration-backup-*.json`).
|
||||||
379
scripts/helcim-plan-consolidation.js
Normal file
379
scripts/helcim-plan-consolidation.js
Normal file
|
|
@ -0,0 +1,379 @@
|
||||||
|
/**
|
||||||
|
* One-off migration: consolidate Helcim payment plans from per-tier model
|
||||||
|
* to unified Monthly/Annual Membership plans and backfill Mongo members.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/helcim-plan-consolidation.js # dry-run (default)
|
||||||
|
* node scripts/helcim-plan-consolidation.js --confirm # execute all steps
|
||||||
|
* node scripts/helcim-plan-consolidation.js --mongo-only # Mongo cleanup only (no Helcim)
|
||||||
|
*
|
||||||
|
* Required env: HELCIM_API_TOKEN, MONGODB_URI
|
||||||
|
*/
|
||||||
|
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
import { dirname, resolve } from 'path'
|
||||||
|
import { writeFileSync } from 'fs'
|
||||||
|
import mongoose from 'mongoose'
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = dirname(__filename)
|
||||||
|
dotenv.config({ path: resolve(__dirname, '../.env') })
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Config
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const HELCIM_API_BASE = 'https://api.helcim.com/v2'
|
||||||
|
|
||||||
|
// Legacy plan IDs to delete
|
||||||
|
const LEGACY_PLAN_IDS = ['20162', '21596', '21597', '21598']
|
||||||
|
|
||||||
|
// Legacy plan name pattern — catch any additional per-tier plans
|
||||||
|
const LEGACY_PLAN_NAME_RE = /^Ghost Guild\s*[-–]\s*Member/i
|
||||||
|
|
||||||
|
const MONTHLY_PLAN = {
|
||||||
|
name: 'Monthly Membership',
|
||||||
|
recurringAmount: 15,
|
||||||
|
billingPeriod: 'monthly',
|
||||||
|
}
|
||||||
|
|
||||||
|
const ANNUAL_PLAN = {
|
||||||
|
name: 'Annual Membership',
|
||||||
|
recurringAmount: 150,
|
||||||
|
billingPeriod: 'yearly',
|
||||||
|
}
|
||||||
|
|
||||||
|
const HELCIM_PLAN_PAYLOAD_BASE = {
|
||||||
|
description: 'Ghost Guild membership',
|
||||||
|
type: 'subscription',
|
||||||
|
status: 'active',
|
||||||
|
currency: 'CAD',
|
||||||
|
setupAmount: 0,
|
||||||
|
billingPeriodIncrements: 1,
|
||||||
|
dateBilling: 'Sign-up',
|
||||||
|
termType: 'forever',
|
||||||
|
freeTrialPeriod: 0,
|
||||||
|
taxType: 'customer',
|
||||||
|
paymentMethod: 'card',
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Flag parsing
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const args = process.argv.slice(2)
|
||||||
|
const CONFIRM = args.includes('--confirm')
|
||||||
|
const MONGO_ONLY = args.includes('--mongo-only')
|
||||||
|
|
||||||
|
// --mongo-only implies destructive Mongo ops even without --confirm
|
||||||
|
// --confirm enables all destructive ops (Helcim + Mongo)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helcim helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function helcimHeaders() {
|
||||||
|
const token = process.env.HELCIM_API_TOKEN
|
||||||
|
if (!token) throw new Error('HELCIM_API_TOKEN not set in .env')
|
||||||
|
return {
|
||||||
|
accept: 'application/json',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'api-token': token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function helcimGet(path) {
|
||||||
|
const res = await fetch(`${HELCIM_API_BASE}${path}`, {
|
||||||
|
headers: helcimHeaders(),
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text()
|
||||||
|
throw new Error(`GET ${path} failed (${res.status}): ${body}`)
|
||||||
|
}
|
||||||
|
return res.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function helcimDelete(path) {
|
||||||
|
const res = await fetch(`${HELCIM_API_BASE}${path}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: helcimHeaders(),
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text()
|
||||||
|
throw new Error(`DELETE ${path} failed (${res.status}): ${body}`)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
async function helcimPost(path, payload) {
|
||||||
|
const res = await fetch(`${HELCIM_API_BASE}${path}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: helcimHeaders(),
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
const body = await res.text()
|
||||||
|
let parsed
|
||||||
|
try { parsed = JSON.parse(body) } catch { parsed = body }
|
||||||
|
return { ok: res.ok, status: res.status, body: parsed }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Mongo helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function connectMongo() {
|
||||||
|
const uri = process.env.MONGODB_URI
|
||||||
|
if (!uri) throw new Error('MONGODB_URI not set in .env')
|
||||||
|
await mongoose.connect(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Timestamp helper
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function safeTimestamp() {
|
||||||
|
return new Date().toISOString().replace(/:/g, '-')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Main
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('=== Helcim Plan Consolidation' + (CONFIRM ? '' : ' (DRY RUN)') + ' ===')
|
||||||
|
console.log(`Mode: ${CONFIRM ? 'LIVE' : 'dry-run'}`)
|
||||||
|
console.log(`Flags: --confirm=${CONFIRM}, --mongo-only=${MONGO_ONLY}`)
|
||||||
|
console.log()
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Step 1: Backup
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
console.log('Step 1: Backup')
|
||||||
|
|
||||||
|
let subscriptions = []
|
||||||
|
let plans = []
|
||||||
|
let mongoMembers = []
|
||||||
|
|
||||||
|
if (!MONGO_ONLY) {
|
||||||
|
process.stdout.write(' Fetching Helcim subscriptions... ')
|
||||||
|
const subData = await helcimGet('/subscriptions')
|
||||||
|
subscriptions = Array.isArray(subData) ? subData : (subData.subscriptions ?? subData.data ?? [])
|
||||||
|
console.log(`Found ${subscriptions.length} subscriptions`)
|
||||||
|
|
||||||
|
process.stdout.write(' Fetching Helcim payment plans... ')
|
||||||
|
const planData = await helcimGet('/payment-plans')
|
||||||
|
plans = Array.isArray(planData) ? planData : (planData.plans ?? planData.data ?? [])
|
||||||
|
console.log(`Found ${plans.length} plans`)
|
||||||
|
} else {
|
||||||
|
console.log(' [--mongo-only] Skipping Helcim backup')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always connect Mongo for member backup
|
||||||
|
process.stdout.write(' Fetching Mongo members with helcimSubscriptionId... ')
|
||||||
|
await connectMongo()
|
||||||
|
const membersColl = mongoose.connection.collection('members')
|
||||||
|
const rawMembers = await membersColl
|
||||||
|
.find({ helcimSubscriptionId: { $exists: true } })
|
||||||
|
.project({ _id: 1, email: 1, status: 1, helcimSubscriptionId: 1, contributionTier: 1, billingCadence: 1 })
|
||||||
|
.toArray()
|
||||||
|
mongoMembers = rawMembers
|
||||||
|
console.log(`Found ${mongoMembers.length} members`)
|
||||||
|
|
||||||
|
const backupFilename = `.migration-backup-${safeTimestamp()}.json`
|
||||||
|
const backupData = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
flags: { confirm: CONFIRM, mongoOnly: MONGO_ONLY },
|
||||||
|
helcimSubscriptions: subscriptions,
|
||||||
|
helcimPlans: plans,
|
||||||
|
mongoMembers,
|
||||||
|
}
|
||||||
|
writeFileSync(backupFilename, JSON.stringify(backupData, null, 2))
|
||||||
|
console.log(` Backup written: ${backupFilename}`)
|
||||||
|
console.log()
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Step 2: Delete subscriptions
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
if (!MONGO_ONLY) {
|
||||||
|
console.log('Step 2: Delete subscriptions')
|
||||||
|
if (!CONFIRM) {
|
||||||
|
console.log(` [DRY RUN] Would delete ${subscriptions.length} subscriptions:`)
|
||||||
|
for (const sub of subscriptions) {
|
||||||
|
const memberEmail = sub.customer?.email ?? sub.email ?? sub.customerId ?? 'unknown'
|
||||||
|
console.log(` - ${sub.id} (member: ${memberEmail})`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(` Deleting ${subscriptions.length} subscriptions...`)
|
||||||
|
for (const sub of subscriptions) {
|
||||||
|
try {
|
||||||
|
await helcimDelete(`/subscriptions/${sub.id}`)
|
||||||
|
console.log(` [OK] Deleted subscription ${sub.id}`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(` [ERROR] Failed to delete subscription ${sub.id}: ${err.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log()
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Step 3: Delete legacy plans
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
if (!MONGO_ONLY) {
|
||||||
|
console.log('Step 3: Delete legacy plans')
|
||||||
|
|
||||||
|
// Detect any extra legacy plans by name pattern
|
||||||
|
const extraLegacyIds = plans
|
||||||
|
.filter((p) => {
|
||||||
|
const idStr = String(p.id)
|
||||||
|
return !LEGACY_PLAN_IDS.includes(idStr) && LEGACY_PLAN_NAME_RE.test(p.name)
|
||||||
|
})
|
||||||
|
.map((p) => String(p.id))
|
||||||
|
|
||||||
|
if (extraLegacyIds.length > 0) {
|
||||||
|
console.log(` Detected additional legacy plans by name pattern: ${extraLegacyIds.join(', ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const allLegacyIds = [...new Set([...LEGACY_PLAN_IDS, ...extraLegacyIds])]
|
||||||
|
|
||||||
|
if (!CONFIRM) {
|
||||||
|
console.log(` [DRY RUN] Would delete ${allLegacyIds.length} plans: ${allLegacyIds.join(', ')}`)
|
||||||
|
} else {
|
||||||
|
console.log(` Deleting ${allLegacyIds.length} plans: ${allLegacyIds.join(', ')}`)
|
||||||
|
for (const planId of allLegacyIds) {
|
||||||
|
try {
|
||||||
|
await helcimDelete(`/payment-plans/${planId}`)
|
||||||
|
console.log(` [OK] Deleted plan ${planId}`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(` [ERROR] Failed to delete plan ${planId}: ${err.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log()
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Steps 4 & 5: Create new plans
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
let monthlyPlanId = null
|
||||||
|
let annualPlanId = null
|
||||||
|
|
||||||
|
if (!MONGO_ONLY) {
|
||||||
|
// Idempotency: check if plans already exist in backup
|
||||||
|
const existingMonthly = plans.find((p) => p.name === MONTHLY_PLAN.name)
|
||||||
|
const existingAnnual = plans.find((p) => p.name === ANNUAL_PLAN.name)
|
||||||
|
|
||||||
|
// Step 4: Monthly Membership
|
||||||
|
console.log('Step 4: Create Monthly Membership')
|
||||||
|
const monthlyPayload = {
|
||||||
|
...HELCIM_PLAN_PAYLOAD_BASE,
|
||||||
|
name: MONTHLY_PLAN.name,
|
||||||
|
recurringAmount: MONTHLY_PLAN.recurringAmount,
|
||||||
|
billingPeriod: MONTHLY_PLAN.billingPeriod,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingMonthly) {
|
||||||
|
monthlyPlanId = existingMonthly.id
|
||||||
|
console.log(` [SKIP] Plan "${MONTHLY_PLAN.name}" already exists with id ${monthlyPlanId}`)
|
||||||
|
} else if (!CONFIRM) {
|
||||||
|
console.log(` [DRY RUN] Would POST /payment-plans with:`)
|
||||||
|
console.log(' ', JSON.stringify(monthlyPayload, null, 4).split('\n').join('\n '))
|
||||||
|
} else {
|
||||||
|
console.log(` POSTing /payment-plans with:`)
|
||||||
|
console.log(' ', JSON.stringify(monthlyPayload, null, 4).split('\n').join('\n '))
|
||||||
|
const result = await helcimPost('/payment-plans', monthlyPayload)
|
||||||
|
if (!result.ok) {
|
||||||
|
console.error(` [ERROR] POST failed (${result.status}):`)
|
||||||
|
console.error(' ', JSON.stringify(result.body, null, 2))
|
||||||
|
console.error(' Aborting — fix the payload and re-run.')
|
||||||
|
await mongoose.disconnect()
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
monthlyPlanId = result.body.id
|
||||||
|
console.log(` [OK] Created Monthly Membership plan id: ${monthlyPlanId}`)
|
||||||
|
}
|
||||||
|
console.log()
|
||||||
|
|
||||||
|
// Step 5: Annual Membership
|
||||||
|
console.log('Step 5: Create Annual Membership')
|
||||||
|
const annualPayload = {
|
||||||
|
...HELCIM_PLAN_PAYLOAD_BASE,
|
||||||
|
name: ANNUAL_PLAN.name,
|
||||||
|
recurringAmount: ANNUAL_PLAN.recurringAmount,
|
||||||
|
billingPeriod: ANNUAL_PLAN.billingPeriod,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingAnnual) {
|
||||||
|
annualPlanId = existingAnnual.id
|
||||||
|
console.log(` [SKIP] Plan "${ANNUAL_PLAN.name}" already exists with id ${annualPlanId}`)
|
||||||
|
} else if (!CONFIRM) {
|
||||||
|
console.log(` [DRY RUN] Would POST /payment-plans with:`)
|
||||||
|
console.log(' ', JSON.stringify(annualPayload, null, 4).split('\n').join('\n '))
|
||||||
|
console.log(' Note: billingPeriod="yearly" is Helcim v2 convention.')
|
||||||
|
console.log(' If 4xx on --confirm, check the Helcim API docs for the correct field value.')
|
||||||
|
} else {
|
||||||
|
console.log(` POSTing /payment-plans with:`)
|
||||||
|
console.log(' ', JSON.stringify(annualPayload, null, 4).split('\n').join('\n '))
|
||||||
|
const result = await helcimPost('/payment-plans', annualPayload)
|
||||||
|
if (!result.ok) {
|
||||||
|
console.error(` [ERROR] POST failed (${result.status}):`)
|
||||||
|
console.error(' ', JSON.stringify(result.body, null, 2))
|
||||||
|
console.error(' If billingPeriod is wrong, update ANNUAL_PLAN.billingPeriod and re-run.')
|
||||||
|
console.error(' Aborting.')
|
||||||
|
await mongoose.disconnect()
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
annualPlanId = result.body.id
|
||||||
|
console.log(` [OK] Created Annual Membership plan id: ${annualPlanId}`)
|
||||||
|
}
|
||||||
|
console.log()
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Step 6: Mongo cleanup
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
const runMongoDestructive = CONFIRM || MONGO_ONLY
|
||||||
|
console.log('Step 6: Mongo cleanup')
|
||||||
|
|
||||||
|
if (!runMongoDestructive) {
|
||||||
|
console.log(` [DRY RUN] Would updateMany on ${mongoMembers.length} members:`)
|
||||||
|
console.log(' { $set: { contributionTier: \'0\', billingCadence: \'monthly\' }, $unset: { helcimSubscriptionId: \'\' } }')
|
||||||
|
console.log(' (Status field is NOT changed — active stays active, pending_payment stays pending_payment)')
|
||||||
|
} else {
|
||||||
|
console.log(` Running updateMany on ${mongoMembers.length} members with helcimSubscriptionId...`)
|
||||||
|
const result = await membersColl.updateMany(
|
||||||
|
{ helcimSubscriptionId: { $exists: true } },
|
||||||
|
{
|
||||||
|
$set: { contributionTier: '0', billingCadence: 'monthly' },
|
||||||
|
$unset: { helcimSubscriptionId: '' },
|
||||||
|
},
|
||||||
|
{ runValidators: false },
|
||||||
|
)
|
||||||
|
console.log(` [OK] Modified ${result.modifiedCount} member(s)`)
|
||||||
|
}
|
||||||
|
console.log()
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Summary
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
if (!CONFIRM && !MONGO_ONLY) {
|
||||||
|
console.log('=== To execute, re-run with --confirm ===')
|
||||||
|
console.log('=== To run Mongo cleanup only, re-run with --mongo-only ===')
|
||||||
|
} else {
|
||||||
|
if (!MONGO_ONLY) {
|
||||||
|
console.log('=== New plan IDs — copy into .env ===')
|
||||||
|
console.log(`NUXT_HELCIM_MONTHLY_PLAN_ID=${monthlyPlanId ?? '<check backup>'}`)
|
||||||
|
console.log(`NUXT_HELCIM_ANNUAL_PLAN_ID=${annualPlanId ?? '<check backup>'}`)
|
||||||
|
}
|
||||||
|
console.log('=== Migration complete ===')
|
||||||
|
}
|
||||||
|
|
||||||
|
await mongoose.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('\n[FATAL]', err.message)
|
||||||
|
mongoose.disconnect().catch(() => {})
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue