Skip to content

Writing a resolver

A resolver answers the last-mile question: now that the right database exists, how does the app find it? It’s the smallest axis — one method — and the easiest first adapter to write.

interface ConnectionResolver {
readonly id: string;
readonly apiVersion: number;
inject(connection: ConnectionString): Promise<void>;
}

After inject(connection) resolves, a process started the way the project normally starts (reading .env, evaluating .envrc, whatever your mechanism is) must observe connection as the active database connection. Two practical implications:

  • Injection repeats on every switch, so it must overwrite its previous value, not append a second one.
  • Be surgical. If you write into a file the user also owns, upsert your one key and leave every other line untouched.

The real direnv resolver — upsert one export line into .envrc:

import { readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import type { ConnectionResolver } from 'branchly';
export interface DirenvResolverOptions {
readonly file?: string;
readonly key?: string;
readonly cwd?: string;
}
export const upsertExport = (content: string, key: string, value: string): string => {
const line = `export ${key}=${value}`;
const existing = content.length === 0 ? [] : content.replace(/\n+$/, '').split('\n');
const replaced = existing.map((current) => (current.startsWith(`export ${key}=`) ? line : current));
const next = replaced.includes(line) ? replaced : [...replaced, line];
return `${next.join('\n')}\n`;
};
const readExisting = (path: string): Promise<string> =>
readFile(path, 'utf8').then(
(content) => content,
() => '',
);
export const createDirenvResolver = (options: DirenvResolverOptions = {}): ConnectionResolver => {
const path = join(options.cwd ?? '.', options.file ?? '.envrc');
const key = options.key ?? 'DATABASE_URL';
return {
id: 'direnv',
apiVersion: 1,
inject: async (connection) => {
const existing = await readExisting(path);
await writeFile(path, upsertExport(existing, key, connection), 'utf8');
},
};
};
export default createDirenvResolver;

Note the cwd option: file-based resolvers should take one so tests can point them at a temp directory.

Wire up the conformance kit — you provide an observe function that reads the connection back the way an app would, and the kit verifies injection is observable and that a second inject overwrites the first.