import type { Result } from '@prisma-next/utils/result'; import { notOk, ok } from '@prisma-next/utils/result'; import { validateRefName } from '../refs'; import type { ContractRef, ContractRefProvenance, RefResolutionContext, RefResolutionError, } from './types'; import { findEdgeByDirName, isFullHash, isHexPrefix, normalizeHashPrefix } from './types'; /** * Resolve a user-supplied string to a contract hash using the unified * contract-reference grammar. * * Accepted forms: * - `@contract` — the on-disk working contract hash (offline; requires * `ctx.contractHash` to be set) * - `@db` — the live database marker (connection-required); callers MUST * check `result.value.provenance.kind === 'reserved-db'` and resolve the * actual hash via `readAllMarkers()` before using `result.value.hash` * - Full storage hash (`sha256:<64 hex>` or `sha256:empty`) * - Hex prefix (6+ hex chars, must uniquely identify one contract) * - Ref name (looked up in the refs index) * - Migration directory name (resolves to the migration's `to`-contract) * - `^` (resolves to the migration's `from`-contract) */ export function parseContractRef( input: string, ctx: RefResolutionContext, ): Result { if (!input) { return notOk({ kind: 'invalid-format', input, reason: 'Reference cannot be empty' }); } if (input === '@contract') { if (ctx.contractHash === undefined) { return notOk({ kind: 'not-found', input, grammar: 'contract', }); } return ok({ hash: ctx.contractHash, provenance: { kind: 'reserved-contract' } }); } if (input === '@db') { // The live DB marker is not available offline. Return a sentinel result with // a `reserved-db` provenance; callers must resolve the actual hash via // `readAllMarkers()`. The `hash` placeholder is intentionally empty — it // must NOT be used directly. This is enforced by convention; callers // should check `provenance.kind` before using the hash. return ok({ hash: '', provenance: { kind: 'reserved-db' } }); } if (isFullHash(input)) { if (ctx.graph.nodes.has(input)) { return ok({ hash: input, provenance: { kind: 'hash', input } }); } return notOk({ kind: 'not-found', input, grammar: 'contract' }); } if (input.endsWith('^')) { const dirName = input.slice(0, -1); if (!dirName) { return notOk({ kind: 'invalid-format', input, reason: 'Missing directory name before ^' }); } const edge = findEdgeByDirName(ctx.graph, dirName); if (edge) { return ok({ hash: edge.from, provenance: { kind: 'migration-from', dirName } }); } return notOk({ kind: 'not-found', input, grammar: 'contract' }); } type Candidate = { hash: string; provenance: ContractRefProvenance; label: string }; const candidates: Candidate[] = []; if (validateRefName(input) && Object.hasOwn(ctx.refs, input)) { const ref = ctx.refs[input]; if (ref) { candidates.push({ hash: ref.hash, provenance: { kind: 'ref', refName: input }, label: `ref "${input}"`, }); } } const edge = findEdgeByDirName(ctx.graph, input); if (edge) { candidates.push({ hash: edge.to, provenance: { kind: 'migration-to', dirName: input }, label: `migration directory "${input}"`, }); } if (isHexPrefix(input)) { const prefix = normalizeHashPrefix(input); const matches = [...ctx.graph.nodes].filter((n) => n.startsWith(prefix)); const [firstMatch] = matches; if (matches.length === 1 && firstMatch !== undefined) { candidates.push({ hash: firstMatch, provenance: { kind: 'hash', input }, label: `hash prefix "${input}"`, }); } else if (matches.length > 1) { return notOk({ kind: 'ambiguous', input, candidates: matches, grammar: 'contract' }); } } const [firstCandidate] = candidates; if (candidates.length === 1 && firstCandidate !== undefined) { return ok({ hash: firstCandidate.hash, provenance: firstCandidate.provenance }); } if (candidates.length > 1) { return notOk({ kind: 'ambiguous', input, candidates: candidates.map((c) => c.label), grammar: 'contract', }); } return notOk({ kind: 'not-found', input, grammar: 'contract' }); }