/* * What an area id is — the host's answer for `external: 'space.area'`. * * The contract declares a location's parent as another location or, when no location has that id, an area of the * space (`axis-or-external`), and says the host resolves the external half. Nothing registered a resolver, so on * 2026-09-18 every location in the running twin showed its parent as "no component can resolve this relation" — * the areas were in this package's own table all along. * * The lookup is (domain, space, areaId), the table's own unique key: an `areaId` is unique inside a space, so a * tenant can hold the same id in two spaces. The space comes from the twin the relation belongs to, which is why * `resolve` is given the instance (external-resolver.ts `ExternalResolveContext`). Looking up by tenant alone * would answer another space's name as this location's parent. * * No `href`: the screen that opens an area belongs to the product app, and putting its route here would fix a * product screen's address in this package. With no href the screen shows the name and makes no link — that is * what the field is for (no dead links). A route is registered the day a screen needs it. */ import { getRepository } from '@things-factory/shell' import { In } from 'typeorm' import { registerExternalResolver, type ExternalRef, type ExternalResolveContext } from '../twin-model/external-resolver.js' import { TwinInstance } from '../twin-instance/twin-instance.js' import { TwinSpace } from './twin-space.js' import { TwinArea } from './twin-area.js' /** The contract's word for this target. Must match `target.entity` or it attaches to no relation. */ export const SPACE_AREA_ENTITY = 'space.area' export async function resolveAreas( domainId: string, ids: readonly string[], context: ExternalResolveContext, warn?: (msg: string) => void ): Promise { const wanted = [...new Set(ids.map(String).filter(Boolean))] if (!wanted.length) return [] /* Which space is this twin in? A twin with no space has no areas to point at — every id is then missing. */ const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId } as any, instanceId: context.instanceId } }) const spaceId = inst?.spaceId if (!spaceId) { warn?.(`[area-resolver] twin "${context.instanceId}" is in no space — its parent ids cannot be areas`) return wanted.map(id => ({ id, missing: true })) } const space = await getRepository(TwinSpace).findOne({ where: { domain: { id: domainId } as any, spaceId } }) if (!space) { warn?.(`[area-resolver] space "${spaceId}" of twin "${context.instanceId}" is not in this tenant`) return wanted.map(id => ({ id, missing: true })) } const rows = await getRepository(TwinArea).find({ where: { domain: { id: domainId } as any, space: { id: space.id } as any, areaId: In(wanted) } }) const byId = new Map(rows.map(r => [r.areaId, r])) /* An id that is in no area of this space is said to be missing — it is not dropped from the answer. */ return wanted.map(id => { const area = byId.get(id) if (!area) return { id, missing: true } return area.name ? { id, name: area.name } : { id } }) } /** Registered at boot, once — the same shape as the reference link resolver. */ export function installAreaResolver(warn?: (msg: string) => void): void { registerExternalResolver( { entity: SPACE_AREA_ENTITY, resolve: (domainId, ids, context) => resolveAreas(domainId, ids, context, warn) }, warn ) }