import { createRequire } from 'module'; import PocketBase from 'pocketbase'; const require = createRequire(import.meta.url); let runtimeConfig = {}; try { runtimeConfig = require('../pocket-base/bai_api_pb_hooks/bai_api_shared/config/runtime.js'); } catch (_error) { runtimeConfig = {}; } const PB_URL = String(process.env.PB_URL || 'https://bai-api.blv-oa.com/pb').replace(/\/+$/, ''); const AUTH_TOKEN = process.env.POCKETBASE_AUTH_TOKEN || runtimeConfig.POCKETBASE_AUTH_TOKEN || ''; const COLLECTION = 'tbl_cart'; const ACTIVE_UNIQUE_INDEX = 'CREATE UNIQUE INDEX idx_tbl_cart_owner_product_active_unique ON tbl_cart (cart_owner, cart_product_id) WHERE is_delete = 0'; const TEMP_RULE = '@request.auth.id != ""'; if (!AUTH_TOKEN) { console.error('Missing POCKETBASE_AUTH_TOKEN'); process.exit(1); } const pb = new PocketBase(PB_URL); function buildCollectionPayload(base, overrides = {}) { const rawFields = Array.isArray(base.fields) ? base.fields : []; const safeFields = rawFields.filter((field) => field && field.name !== 'id'); return { name: base.name, type: base.type, listRule: Object.prototype.hasOwnProperty.call(overrides, 'listRule') ? overrides.listRule : base.listRule, viewRule: Object.prototype.hasOwnProperty.call(overrides, 'viewRule') ? overrides.viewRule : base.viewRule, createRule: Object.prototype.hasOwnProperty.call(overrides, 'createRule') ? overrides.createRule : base.createRule, updateRule: Object.prototype.hasOwnProperty.call(overrides, 'updateRule') ? overrides.updateRule : base.updateRule, deleteRule: Object.prototype.hasOwnProperty.call(overrides, 'deleteRule') ? overrides.deleteRule : base.deleteRule, fields: safeFields, indexes: Object.prototype.hasOwnProperty.call(overrides, 'indexes') ? overrides.indexes : (base.indexes || []), }; } async function setTempRules(collection) { const payload = buildCollectionPayload(collection, { listRule: TEMP_RULE, viewRule: TEMP_RULE, createRule: TEMP_RULE, updateRule: TEMP_RULE, deleteRule: TEMP_RULE, }); await pb.collections.update(collection.id, payload); } async function restoreRules(collection) { await pb.collections.update(collection.id, buildCollectionPayload(collection)); } function groupKey(record) { const owner = String(record.cart_owner || '').trim(); const product = String(record.cart_product_id || '').trim(); return `${owner}||${product}`; } async function dedupeActiveRows() { const rows = await pb.collection(COLLECTION).getFullList({ filter: 'is_delete = 0', sort: '-updated', fields: 'id,cart_owner,cart_product_id,is_delete,updated', }); const groups = new Map(); for (const row of rows) { const key = groupKey(row); if (!groups.has(key)) groups.set(key, []); groups.get(key).push(row); } let duplicateGroups = 0; let softDeleted = 0; for (const [, items] of groups) { if (items.length <= 1) continue; duplicateGroups += 1; const keep = items[0]; for (let i = 1; i < items.length; i += 1) { const row = items[i]; await pb.collection(COLLECTION).update(row.id, { is_delete: 1 }); softDeleted += 1; } console.log(`dedupe group keep=${keep.id} deleted=${items.length - 1}`); } return { total: rows.length, duplicateGroups, softDeleted }; } async function applyActiveUniqueIndex() { const collection = await pb.collections.getOne(COLLECTION); const indexes = Array.isArray(collection.indexes) ? collection.indexes.slice() : []; if (!indexes.includes(ACTIVE_UNIQUE_INDEX)) { indexes.push(ACTIVE_UNIQUE_INDEX); } const payload = buildCollectionPayload(collection, { indexes }); await pb.collections.update(collection.id, payload); const latest = await pb.collections.getOne(COLLECTION); const ok = Array.isArray(latest.indexes) && latest.indexes.includes(ACTIVE_UNIQUE_INDEX); if (!ok) { throw new Error('Active unique index was not applied.'); } } async function main() { pb.authStore.save(AUTH_TOKEN, null); console.log(`connect ${PB_URL}`); const original = await pb.collections.getOne(COLLECTION); try { await setTempRules(original); const dedupe = await dedupeActiveRows(); console.log(JSON.stringify(dedupe, null, 2)); } finally { const latest = await pb.collections.getOne(COLLECTION); const restoreBase = { ...latest, listRule: original.listRule, viewRule: original.viewRule, createRule: original.createRule, updateRule: original.updateRule, deleteRule: original.deleteRule, }; await restoreRules(restoreBase); } await applyActiveUniqueIndex(); console.log('active unique index applied'); } main().catch((error) => { console.error('migration failed', error?.response || error?.message || error); process.exitCode = 1; });