import type { ParseClassSchema, ParseFieldDefinition } from './parse-rest.client.js'; import { ParseRestClient } from './parse-rest.client.js'; const string = (required = false): ParseFieldDefinition => ({ type: 'String', required }); const number = (required = false): ParseFieldDefinition => ({ type: 'Number', required }); const boolean = (required = false): ParseFieldDefinition => ({ type: 'Boolean', required }); const date = (required = false): ParseFieldDefinition => ({ type: 'Date', required }); const object = (required = false): ParseFieldDefinition => ({ type: 'Object', required }); const array = (required = false): ParseFieldDefinition => ({ type: 'Array', required }); const indexes = (prefix: string, ...fields: string[]): Record> => Object.fromEntries( fields.map((field) => [`${prefix}_${field.toLowerCase()}_idx`, { [field]: 1 }]), ); export const VOC_PARSE_CLASSES = { workspace: 'VocWorkspace', sourceConnection: 'VocSourceConnection', importBatch: 'VocImportBatch', product: 'VocProduct', dailyMetric: 'VocDailyMetric', productRelation: 'VocProductRelation', review: 'VocReview', syncJob: 'VocSyncJob', syncJobEvent: 'VocSyncJobEvent', workspaceMember: 'VocWorkspaceMember', analysisRun: 'VocAnalysisRun', insightDecision: 'VocInsightDecision', actionItem: 'VocActionItem', alert: 'VocAlert', auditLog: 'VocAuditLog', promptConfig: 'VocPromptConfig', productKnowledge: 'VocProductKnowledge', listingSourceSnapshot: 'VocListingSourceSnapshot', listingScoreJob: 'VocListingScoreJob', listingScoreItem: 'VocListingScoreItem', listingCurrentScore: 'VocListingCurrentScore', listingVersion: 'VocListingVersion', competitorListingSnapshot: 'VocCompetitorListingSnapshot', competitorListingChange: 'VocCompetitorListingChange', competitorListingRefreshRun: 'VocCompetitorListingRefreshRun', competitorOptimizationTask: 'VocCompetitorOptimizationTask', } as const; export const JD_VOC_SCORE_SLOTS = ['jd_voc_rules', 'jd_voc_hybrid_ai'] as const; export const VOC_PARSE_SCHEMAS: ParseClassSchema[] = [ { className: VOC_PARSE_CLASSES.workspace, fields: { publicId: string(true), name: string(true), caseName: string(true), status: string(true), }, indexes: indexes('voc_workspace', 'publicId', 'status'), }, { className: VOC_PARSE_CLASSES.sourceConnection, fields: { naturalKey: string(true), workspaceId: string(true), platform: string(true), connectionKind: string(true), status: string(true), metadata: object(true), lastCheckedAt: date(), }, indexes: indexes('voc_source', 'naturalKey', 'workspaceId', 'platform', 'status'), }, { className: VOC_PARSE_CLASSES.importBatch, fields: { publicId: string(true), workspaceId: string(true), platform: string(true), sourceKind: string(true), sourceFile: string(), sourceHash: string(), sheets: array(true), status: string(true), totalRows: number(true), successRows: number(true), failedRows: number(true), productRows: number(true), metricRows: number(true), relationRows: number(true), reviewRows: number(true), startedAt: date(), completedAt: date(), sourceDateStart: string(), sourceDateEnd: string(), dailyTotals: array(true), quality: object(true), }, indexes: indexes('voc_import', 'publicId', 'workspaceId', 'platform', 'sourceHash', 'status', 'createdAt'), }, { className: VOC_PARSE_CLASSES.product, fields: { naturalKey: string(true), workspaceId: string(true), platform: string(true), productId: string(true), productKey: string(true), role: string(true), brand: string(), title: string(), model: string(), category1: string(), category2: string(), category3: string(), source: string(true), relationCount: number(true), summary: object(true), trend: array(true), rawPayload: object(), }, indexes: indexes('voc_product', 'naturalKey', 'workspaceId', 'platform', 'productId', 'productKey', 'role', 'category2', 'category3', 'source'), }, { className: VOC_PARSE_CLASSES.dailyMetric, fields: { naturalKey: string(true), workspaceId: string(true), platform: string(true), productId: string(true), metricDate: string(true), source: string(true), gmv: number(true), soldUnits: number(true), transactionOrders: number(true), transactionCustomers: number(true), impressions: number(true), clicks: number(true), views: number(true), visitors: number(true), cartUnits: number(true), orderAmount: number(true), orderUnits: number(true), orderCount: number(true), refundAmount: number(true), refundUnits: number(true), refundOrders: number(true), }, indexes: indexes('voc_metric', 'naturalKey', 'workspaceId', 'platform', 'productId', 'metricDate'), }, { className: VOC_PARSE_CLASSES.productRelation, fields: { naturalKey: string(true), workspaceId: string(true), platform: string(true), relationKey: string(true), ownProductId: string(true), ownProductKey: string(true), competitorProductId: string(true), competitorProductKey: string(true), competitorBrand: string(), category: string(), ownModel: string(), ownCategory1: string(), ownCategory2: string(), ownCategory3: string(), }, indexes: indexes('voc_relation', 'naturalKey', 'workspaceId', 'platform', 'ownProductId', 'competitorProductId'), }, { className: VOC_PARSE_CLASSES.review, fields: { naturalKey: string(true), workspaceId: string(true), platform: string(true), productId: string(true), sourceReviewId: string(), reviewKey: string(true), rating: number(), content: string(true), reviewDate: date(), rawPayload: object(), }, indexes: indexes('voc_review', 'naturalKey', 'workspaceId', 'platform', 'productId', 'reviewDate'), }, { className: VOC_PARSE_CLASSES.syncJob, fields: { publicId: string(true), workspaceId: string(true), platform: string(true), idempotencyKey: string(true), status: string(true), scopes: array(true), productIds: array(true), progress: number(true), attempts: number(true), maxAttempts: number(true), workerId: string(), errorSummary: string(), requestedAt: date(true), startedAt: date(), completedAt: date(), }, indexes: indexes('voc_job', 'publicId', 'workspaceId', 'platform', 'idempotencyKey', 'status', 'requestedAt'), }, { className: VOC_PARSE_CLASSES.syncJobEvent, fields: { publicId: string(true), workspaceId: string(true), jobPublicId: string(true), level: string(true), eventType: string(true), message: string(true), details: object(true), }, indexes: indexes('voc_job_event', 'publicId', 'workspaceId', 'jobPublicId', 'createdAt'), }, { className: VOC_PARSE_CLASSES.workspaceMember, fields: { naturalKey: string(true), workspaceId: string(true), userId: string(true), email: string(), displayName: string(), role: string(true), status: string(true), productIds: array(), }, indexes: indexes('voc_member', 'naturalKey', 'workspaceId', 'userId', 'role', 'status'), }, { className: VOC_PARSE_CLASSES.analysisRun, fields: { publicId: string(true), workspaceId: string(true), analysisType: string(true), targetKind: string(true), targetKey: string(true), status: string(true), input: object(true), result: object(), evidenceCount: number(true), requestedBy: string(true), errorSummary: string(), requestedAt: date(true), startedAt: date(), completedAt: date(), }, indexes: indexes('voc_analysis', 'publicId', 'workspaceId', 'status', 'targetKey', 'requestedAt'), }, { className: VOC_PARSE_CLASSES.insightDecision, fields: { publicId: string(true), workspaceId: string(true), sourceAnalysisId: string(true), sourceInsightId: string(true), decision: string(true), reviewedEvidenceIds: array(true), comment: string(true), decidedBy: string(true), decidedAt: date(true), version: number(true), supersedesId: string(), isCurrent: boolean(true), }, indexes: { ...indexes( 'voc_insight_decision', 'publicId', 'workspaceId', 'sourceAnalysisId', 'sourceInsightId', 'decision', 'decidedBy', 'decidedAt', 'isCurrent', ), voc_insight_decision_source_current_idx: { workspaceId: 1, sourceAnalysisId: 1, sourceInsightId: 1, isCurrent: 1, }, voc_insight_decision_source_version_idx: { workspaceId: 1, sourceAnalysisId: 1, sourceInsightId: 1, version: -1, }, }, }, { className: VOC_PARSE_CLASSES.actionItem, fields: { publicId: string(true), workspaceId: string(true), sourceAnalysisId: string(), sourceInsightId: string(), sourceDecisionId: string(), sourceKind: string(true), creationKey: string(true), evidenceIds: array(), validationMetric: string(), actionType: string(true), title: string(true), description: string(), priority: string(true), status: string(true), productKey: string(), assigneeUserId: string(), dueAt: date(), createdBy: string(true), completedAt: date(), }, indexes: { ...indexes( 'voc_action', 'publicId', 'workspaceId', 'sourceAnalysisId', 'sourceDecisionId', 'sourceKind', 'creationKey', 'status', 'assigneeUserId', 'createdAt', ), voc_action_workspace_creation_key_idx: { workspaceId: 1, creationKey: 1 }, }, }, { className: VOC_PARSE_CLASSES.alert, fields: { publicId: string(true), workspaceId: string(true), alertType: string(true), severity: string(true), status: string(true), productKey: string(), title: string(true), summary: string(), evidence: array(true), detectedAt: date(true), acknowledgedBy: string(), acknowledgedAt: date(), resolvedAt: date(), }, indexes: indexes('voc_alert', 'publicId', 'workspaceId', 'status', 'productKey', 'detectedAt'), }, { className: VOC_PARSE_CLASSES.auditLog, fields: { publicId: string(true), workspaceId: string(true), actorUserId: string(true), action: string(true), entityType: string(true), entityId: string(), metadata: object(true), }, indexes: indexes('voc_audit', 'publicId', 'workspaceId', 'entityType', 'createdAt'), }, { className: VOC_PARSE_CLASSES.promptConfig, fields: { naturalKey: string(true), workspaceId: string(true), promptKey: string(true), name: string(true), module: string(true), scope: string(true), description: string(), template: string(true), enabled: boolean(true), variables: array(true), dataSources: array(true), targetKey: string(), targetStylesJson: string(), model: string(), revision: number(true), updatedBy: string(true), }, indexes: indexes('voc_prompt', 'naturalKey', 'workspaceId', 'promptKey', 'module', 'scope'), }, { className: VOC_PARSE_CLASSES.productKnowledge, fields: { naturalKey: string(true), workspaceId: string(true), productKey: string(true), productId: string(true), productRole: string(true), featured: boolean(true), status: string(true), tags: array(true), note: string(), ownerUserId: string(), createdBy: string(true), updatedBy: string(true), }, indexes: indexes( 'voc_product_knowledge', 'naturalKey', 'workspaceId', 'productKey', 'productId', 'productRole', 'featured', 'status', 'updatedAt', ), }, { className: VOC_PARSE_CLASSES.listingSourceSnapshot, fields: { naturalKey: string(true), workspaceId: string(true), platform: string(true), shopId: string(true), productId: string(true), sourceHash: string(true), detailStatus: string(true), payload: object(true), title: string(), categoryIds: array(), itemStatus: string(), coverageStatus: string(), isCurrent: boolean(), catalogIncluded: boolean(), catalogCohort: string(), scoreStatus: string(), aiScoreStatus: string(), latestOverallScore: number(), latestKnownScore: number(), latestKnownMaxScore: number(), latestComplianceStatus: string(), latestScoreAt: date(), sourceModifiedAt: date(), observedAt: date(true), }, indexes: indexes('voc_listing_source', 'naturalKey', 'workspaceId', 'platform', 'productId', 'sourceHash', 'isCurrent', 'catalogIncluded', 'catalogCohort', 'coverageStatus', 'scoreStatus', 'aiScoreStatus', 'latestOverallScore', 'observedAt'), }, { className: VOC_PARSE_CLASSES.listingScoreJob, fields: { publicId: string(true), naturalKey: string(true), workspaceId: string(true), platform: string(true), idempotencyKey: string(true), requestHash: string(true), status: string(true), payload: object(true), requestedAt: date(true), }, indexes: indexes('voc_listing_job', 'publicId', 'naturalKey', 'workspaceId', 'status', 'requestedAt'), }, { className: VOC_PARSE_CLASSES.listingScoreItem, fields: { publicId: string(true), naturalKey: string(true), workspaceId: string(true), jobId: string(true), productId: string(true), sourceHash: string(true), status: string(true), payload: object(true), }, indexes: indexes('voc_listing_item', 'publicId', 'naturalKey', 'workspaceId', 'jobId', 'status', 'productId'), }, { className: VOC_PARSE_CLASSES.listingCurrentScore, fields: { publicId: string(true), naturalKey: string(true), workspaceId: string(true), productId: string(true), slot: string(true), sourceHash: string(true), rubricVersion: string(true), overallScore: number(), knownOverallScore: number(), knownOverallMaxScore: number(), scoreKind: string(), aiStatus: string(), complianceStatus: string(), executionKey: string(), inputFingerprint: string(), payload: object(true), scoredAt: date(true), }, indexes: { ...indexes('voc_listing_current_score', 'publicId', 'naturalKey', 'workspaceId', 'productId', 'slot', 'sourceHash', 'executionKey', 'scoredAt'), voc_listing_current_score_rubric_version_idx: { workspaceId: 1, productId: 1, rubricVersion: 1 }, voc_listing_current_score_kind_idx: { workspaceId: 1, productId: 1, scoreKind: 1 }, }, }, { className: VOC_PARSE_CLASSES.listingVersion, fields: { publicId: string(true), naturalKey: string(true), workspaceId: string(true), productId: string(true), versionNo: number(true), baseSourceHash: string(true), status: string(true), payload: object(true), versionCreatedAt: date(true), }, indexes: indexes('voc_listing_version', 'publicId', 'naturalKey', 'workspaceId', 'productId', 'status', 'versionCreatedAt'), }, { className: VOC_PARSE_CLASSES.competitorListingSnapshot, fields: { publicId: string(true), naturalKey: string(true), workspaceId: string(true), platform: string(true), productId: string(true), previousSnapshotId: string(), contentHash: string(true), observedAt: date(true), title: string(), priceCents: number(), currency: string(true), availability: string(true), mainImageUrl: string(), keySpecifications: object(true), observedFields: array(true), collectionStatus: string(true), }, indexes: indexes( 'voc_competitor_listing_snapshot', 'publicId', 'naturalKey', 'workspaceId', 'platform', 'productId', 'previousSnapshotId', 'contentHash', 'observedAt', ), }, { className: VOC_PARSE_CLASSES.competitorListingChange, fields: { publicId: string(true), naturalKey: string(true), workspaceId: string(true), platform: string(true), productId: string(true), previousSnapshotId: string(true), currentSnapshotId: string(true), detectedAt: date(true), changeTypes: array(true), changes: array(true), }, indexes: indexes( 'voc_competitor_listing_change', 'publicId', 'naturalKey', 'workspaceId', 'platform', 'productId', 'previousSnapshotId', 'currentSnapshotId', 'detectedAt', ), }, { className: VOC_PARSE_CLASSES.competitorListingRefreshRun, fields: { publicId: string(true), workspaceId: string(true), platform: string(true), trigger: string(true), status: string(true), total: number(true), completed: number(true), baseline: number(true), unchanged: number(true), changed: number(true), failed: number(true), itemResults: array(true), requestedAt: date(true), startedAt: date(), completedAt: date(), }, indexes: indexes( 'voc_competitor_listing_refresh_run', 'publicId', 'workspaceId', 'platform', 'status', 'requestedAt', ), }, { className: VOC_PARSE_CLASSES.competitorOptimizationTask, fields: { publicId: string(true), workspaceId: string(true), competitorSnapshotId: string(true), ownProductId: string(true), dimension: string(true), creationKey: string(true), status: string(true), impactScore: number(), beforeScore: number(), afterScore: number(), delta: number(), effectiveness: string(), }, indexes: indexes('voc_competitor_optimization_task', 'publicId', 'workspaceId', 'creationKey', 'status', 'competitorSnapshotId', 'ownProductId'), }, ]; export interface ParseSchemaSyncResult { created: string[]; updated: string[]; unchanged: string[]; } export async function ensureVocParseSchemas(client: ParseRestClient): Promise { return ensureParseSchemas(client, VOC_PARSE_SCHEMAS); } export const LISTING_PARSE_SCHEMAS = VOC_PARSE_SCHEMAS.filter((schema) => ( schema.className === VOC_PARSE_CLASSES.listingSourceSnapshot || schema.className === VOC_PARSE_CLASSES.listingScoreJob || schema.className === VOC_PARSE_CLASSES.listingScoreItem || schema.className === VOC_PARSE_CLASSES.listingCurrentScore || schema.className === VOC_PARSE_CLASSES.listingVersion )); export async function ensureListingParseSchemas(client: ParseRestClient): Promise { return ensureParseSchemas(client, LISTING_PARSE_SCHEMAS); } async function ensureParseSchemas(client: ParseRestClient, schemas: ParseClassSchema[]): Promise { const existing = new Map((await client.schemas()).map((schema) => [schema.className, schema])); const result: ParseSchemaSyncResult = { created: [], updated: [], unchanged: [] }; for (const schema of schemas) { const current = existing.get(schema.className); if (!current) { await client.createSchema(schema); result.created.push(schema.className); continue; } const changed = Object.fromEntries( Object.entries(schema.fields).filter(([name, definition]) => { const field = current.fields?.[name]; return !field || field.type !== definition.type || (field.required !== undefined && Boolean(field.required) !== Boolean(definition.required)); }), ); const fieldsChanged = Object.keys(changed).length > 0; const missingIndexes = Object.fromEntries( Object.entries(schema.indexes ?? {}).filter(([name]) => !current.indexes?.[name]), ); const indexesChanged = Object.keys(missingIndexes).length > 0; const permissionsChanged = !hasMasterOnlyPermissions(current.classLevelPermissions); if (fieldsChanged) { await client.addSchemaFields(schema.className, changed); } if (permissionsChanged) { await client.setSchemaPermissions(schema.className); } if (indexesChanged) { await client.addSchemaIndexes(schema.className, missingIndexes); } if (fieldsChanged || permissionsChanged || indexesChanged) { result.updated.push(schema.className); } else { result.unchanged.push(schema.className); } } return result; } export function hasMasterOnlyPermissions( permissions: Record> | undefined, ): boolean { if (!permissions) return false; return ['find', 'get', 'count', 'create', 'update', 'delete', 'addField'].every((operation) => ( Object.hasOwn(permissions, operation) && Object.keys(permissions[operation] ?? {}).length === 0 )); } export function isVocSchemaReady(classNames: Iterable): boolean { const available = new Set(classNames); return VOC_PARSE_SCHEMAS.every((schema) => available.has(schema.className)); } export const requiredVocClassNames = VOC_PARSE_SCHEMAS.map((schema) => schema.className);