import type { ComAtprotoRepoGetRecord, ComAtprotoRepoListRecords } from '@atcute/atproto';
import { type Client, ok } from '@atcute/client';
import type { Blob as AtpBlob, Cid, Did, InferInput, Nsid, ResourceUri } from '@atcute/lexicons';
import type { Records } from '@atcute/lexicons/ambient';
/**
* typed `com.atproto.repo.*` record helpers.
*
* re-types generic record values from `unknown` to their concrete shapes via the ambient `Records` map, and
* centralizes the necessary casts for repo wire shapes.
*/
/** An NSID for which a record type is registered in the ambient `Records` map. */
type RecordType = keyof Records & Nsid;
interface CreateRecordInput<K extends RecordType> {
repo: Did;
collection: K;
record: InferInput<Records[K]>;
rkey?: string;
swapCommit?: string;
validate?: boolean;
}
type CreateRecordOptions<K extends RecordType> = Omit<CreateRecordInput<K>, 'collection' | 'record'> & {
record: InferInput<Records[K]> & { $type: K };
};
/**
* creates a repo record via `com.atproto.repo.createRecord`.
*
* @param client client to issue the call on (use `pds` client for own-repo writes)
* @param options record to create
* @returns create-record response
*/
export const createRecord = async <K extends RecordType>(client: Client, options: CreateRecordOptions<K>) => {
const input: CreateRecordInput<K> = {
...options,
collection: options.record.$type,
};
return await ok(client.post('com.atproto.repo.createRecord', { input }));
};
interface PutRecordInput<K extends RecordType> {
repo: Did;
collection: K;
rkey: string;
record: InferInput<Records[K]>;
swapCommit?: string;
swapRecord?: Cid | null;
validate?: boolean;
}
type PutRecordOptions<K extends RecordType> = Omit<PutRecordInput<K>, 'collection' | 'record'> & {
record: InferInput<Records[K]> & { $type: K };
};
/**
* writes a repo record at a known rkey.
*
* @param client client to issue the call on
* @param options record to write
* @returns put-record response
*/
export const putRecord = async <K extends RecordType>(client: Client, options: PutRecordOptions<K>) => {
const input: PutRecordInput<K> = {
...options,
collection: options.record.$type,
};
return await ok(client.post('com.atproto.repo.putRecord', { input }));
};
interface DeleteRecordOptions<K extends RecordType> {
repo: Did;
collection: K;
rkey: string;
swapCommit?: string;
swapRecord?: string;
}
/**
* deletes a repo record via `com.atproto.repo.deleteRecord`.
*
* @param client client to issue the call on — the `pds` client for own-repo writes
* @param options record to delete
*/
export const deleteRecord = async <K extends RecordType>(client: Client, options: DeleteRecordOptions<K>) => {
await ok(client.post('com.atproto.repo.deleteRecord', { input: options }));
};
interface GetRecordOptions<K extends RecordType> {
repo: Did;
collection: K;
rkey: string;
cid?: string;
signal?: AbortSignal;
}
type GetRecordOutput<T> = ComAtprotoRepoGetRecord.$output & { value: T };
/**
* reads a single repo record via `com.atproto.repo.getRecord`, typed by its `collection`.
*
* @param client client to issue the call on — `appview` for another user's record, `pds` for own.
* @param options record to read.
* @returns record output, with `value` typed from the `collection`.
*/
export const getRecord = async <K extends RecordType>(
client: Client,
options: GetRecordOptions<K>,
): Promise<GetRecordOutput<InferInput<Records[K]>>> => {
const data = await ok(
client.get('com.atproto.repo.getRecord', {
signal: options.signal,
params: {
repo: options.repo,
collection: options.collection,
rkey: options.rkey,
cid: options.cid,
},
}),
);
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the requested collection pins `value`, which the wire type leaves `unknown`
return data as GetRecordOutput<InferInput<Records[K]>>;
};
interface ListRecordsOptions<K extends RecordType> {
repo: Did;
collection: K;
cursor?: string;
limit?: number;
signal?: AbortSignal;
}
type ListRecordsOutput<T> = Omit<ComAtprotoRepoListRecords.$output, 'records'> & {
cursor?: string;
records: { cid: Cid; uri: ResourceUri; value: T }[];
};
/**
* lists repo records via `com.atproto.repo.listRecords`, typed by their `collection`.
*
* @param client client to issue the call on — `appview` for another user's records, `pds` for own
* @param options records to list
* @returns list output, with each record's `value` typed from the `collection`
*/
export const listRecords = async <K extends RecordType>(
client: Client,
options: ListRecordsOptions<K>,
): Promise<ListRecordsOutput<InferInput<Records[K]>>> => {
const data = await ok(
client.get('com.atproto.repo.listRecords', {
signal: options.signal,
params: {
repo: options.repo,
collection: options.collection,
cursor: options.cursor,
limit: options.limit,
},
}),
);
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the requested collection pins each `value`, which the wire type leaves `unknown`
return data as ListRecordsOutput<InferInput<Records[K]>>;
};
/**
* uploads a blob to the user's repo.
*
* @param client the PDS client.
* @param blob the blob to upload.
* @param encoding the blob's mime type, sent as a content-type override; defaults to the blob's own type.
* @returns the resulting blob ref.
*/
export async function uploadBlob(client: Client, blob: Blob, encoding?: string): Promise<AtpBlob> {
const data = await ok(
client.post('com.atproto.repo.uploadBlob', {
headers: encoding ? { 'content-type': encoding } : undefined,
input: blob,
}),
);
return data.blob;
}