| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- 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<string, Record<string, 1>> => 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<ParseSchemaSyncResult> {
- 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<ParseSchemaSyncResult> {
- return ensureParseSchemas(client, LISTING_PARSE_SCHEMAS);
- }
- async function ensureParseSchemas(client: ParseRestClient, schemas: ParseClassSchema[]): Promise<ParseSchemaSyncResult> {
- 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<string, Record<string, unknown>> | 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<string>): 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);
|