/* * 유입 진행 구독 — **트윈을 만드는 동안 무엇을 하고 있는지 화면이 안다.** * * 마스터 읽기는 원본에 여러 질의를 보내는 긴 작업이고, 그 사이 화면은 아무것도 알 수 없었다. 단계를 * 아는 것은 어댑터뿐이므로 어댑터가 **표준 낱말**로 말하고(`REFERENCE_STEPS`) 호스트가 자기 두 단계를 * 더해 이 채널로 흘린다. * * 낱말이 표준이므로 화면은 **단계마다 번역 하나**만 두면 모든 커넥터를 보여 준다. */ import { Arg, Field, Int, ObjectType, Resolver, Root, Subscription } from 'type-graphql' import { assertDomainSubscribeAllowed, getRepository, pubsub } from '@things-factory/shell' import { filter, pipe } from 'graphql-yoga' import { REFERENCE_STEPS } from './reference-progress.js' import { REFERENCE_PROGRESS_TOPIC, referenceProgressFilter, type ReferenceProgressPayload } from './reference-progress-channel.js' import { TwinReference } from './twin-reference.js' @ObjectType({ description: 'One step of progress while a reference is being imported into twins.' }) export class TwinReferenceProgressMessage { @Field({ description: 'Which reference (source) this belongs to.' }) source: string @Field({ description: 'Which site is being built.' }) siteId: string @Field({ description: `Standard step name — one of: ${REFERENCE_STEPS.join(' | ')}.` }) step: string @Field(type => Int, { description: 'Steps finished for this site.' }) done: number @Field(type => Int, { nullable: true, description: 'Total steps for this site. Absent when the adapter did not declare its steps — the UI then shows no percentage rather than inventing one.' }) total?: number @Field(type => Int, { description: 'Index of the site being built (0-based).' }) siteIndex: number @Field(type => Int, { description: 'How many sites are being built.' }) siteCount: number @Field({ nullable: true, description: 'A fact within the step, such as how many rows were read.' }) detail?: string @Field({ nullable: true, description: 'True when this step failed. The UI keeps the progress panel and marks this step.' }) failed?: boolean @Field({ nullable: true, description: 'Why it failed. Present only on failure.' }) error?: string } @Resolver() export class TwinReferenceProgressSubscription { @Subscription(returns => TwinReferenceProgressMessage, { /* * Tenancy is asked twice (2026-09-24): the source must be one of the caller's references, and every * message must be the caller's domain's. It used to filter on `source` alone after checking only that * the caller had a domain — and source names are shared between tenants. */ subscribe: async ({ args, context }) => { const { source } = args const { domain, user } = (context as any)?.state ?? {} await assertDomainSubscribeAllowed(domain, user, (d, u) => process.superUserGranted(d, u)) const owned = await getRepository(TwinReference).findOne({ where: { domain: { id: domain.id }, source } }) if (!owned) throw new Error(`reference not found in this tenant: ${source}`) return pipe(pubsub.subscribe(REFERENCE_PROGRESS_TOPIC), filter(referenceProgressFilter(domain.id, source))) }, description: 'Progress while importing a reference — standard step names, so one translation per step covers every connector.' }) twinReferenceProgress( @Root() payload: ReferenceProgressPayload, @Arg('source') source: string ): TwinReferenceProgressMessage { return payload.twinReferenceProgress } }