import { O as Op } from './ops.d-D9oT7Y5m.cjs'; /** * `@odla-ai/db/import` — plan a bulk upsert from a file or a paste. * * Turning "here are 50,000 rows" into transactions the server will accept is * fiddly in ways that are easy to get subtly wrong: a transaction caps at * {@link MAX_TRANSACTION_OPS} ops AND at a message-size limit, an oversized * single row can never be sent at all, and retrying a partially-applied import * must skip what already committed rather than double-apply it. This module is * the whole of that logic, pure and deterministic, so it can be unit-tested and * shared by every surface that imports data (Studio's panel, `odla-ai app * import`, and your own tooling). * * import { parseImport, buildImportOps, planImportChunks, importMutationId } from "@odla-ai/db/import"; * * const { sources } = parseImport(text, "todos"); * const { ops, rejected } = buildImportOps(sources, { idMode: "field" }); * const runId = crypto.randomUUID(); // once per import, reused on retry * for (const chunk of planImportChunks(ops)) { * await db.transact(chunk.ops, { mutationId: importMutationId(runId, chunk.index) }); * } * * Every op produced is an `update`, which is odla-db's upsert: named attributes * are set and unnamed ones are left alone, so re-importing a row edits it in * place instead of replacing it. */ /** * The most operations a single transaction may carry. * * {@link planImportChunks} packs to this bound so an import never presents the * server with a transaction it is required to reject. Re-exported here because * an importer needs it to reason about chunk counts without depending on the * protocol package directly. */ declare const MAX_TRANSACTION_OPS = 1000; /** * Per-transaction byte budget for {@link planImportChunks}. * * Deliberately below the protocol's 1,100,000-byte message cap: that cap covers * the entire request body, while chunk accounting measures only the ops. The * ~9% headroom absorbs the surrounding envelope, separators, and any UTF-8 * expansion — without it a chunk that fits by our arithmetic is still rejected * on the wire. */ declare const IMPORT_CHUNK_MAX_BYTES = 1000000; /** An import only ever upserts, so its ops are narrower than the full `Op` union. */ type UpdateOp = Extract; /** * How an input row is matched to an entity. * * - `field` — take the entity id from a field on the row (`id` by default). * - `key` — match an existing row by a **unique** attribute, via a lookup ref. * - `generate` — mint a fresh id per row. **Not idempotent**: re-running an * import in this mode creates duplicates rather than updating in place. */ type ImportIdMode = "field" | "key" | "generate"; /** Input shapes {@link parseImport} recognises. */ type ImportFormat = "array" | "jsonl" | "namespace-map" | "object"; /** One input row, the namespace it lands in, and its 1-based position in the input. */ interface ImportSource { ns: string; row: Record; index: number; } /** A row that cannot be imported, identified by its position in the input. */ interface ImportRejection { index: number; reason: string; } /** Detected format plus every row flattened out of it. */ interface ImportParseResult { format: ImportFormat; sources: ImportSource[]; } /** Ops to send, rows that can't be sent, and fields that were dropped. */ interface ImportBuildResult { ops: UpdateOp[]; rejected: ImportRejection[]; /** Server-maintained `$` fields stripped from the input, sorted and unique. */ ignored: string[]; } /** One transaction's worth of ops. */ interface ImportChunk { index: number; ops: UpdateOp[]; bytes: number; } /** Options for {@link buildImportOps}. */ interface ImportOptions { idMode: ImportIdMode; /** * Which field carries the entity id in `field` mode. Defaults to `id`. * Only a literal `id` is stripped from the written attributes (it is already * the entity id); a custom field is kept so it stays queryable. */ idField?: string; /** * The attribute to match on in `key` mode. Must be declared **unique** in the * schema — the server rejects a lookup on a non-unique attribute with * `lookup_not_unique`. */ keyAttr?: string; /** * Whether a namespace declares its own `id` attribute (the "mirror the entity * id so `where: { id }` resolves" pattern). When it does, `generate` mode * fills that attribute from the newly minted id. */ declaresIdAttr?: (ns: string) => boolean; /** Id factory for `generate` mode. Defaults to `crypto.randomUUID`. */ newId?: () => string; /** Overrides {@link IMPORT_CHUNK_MAX_BYTES} when sizing individual rows. */ maxBytes?: number; } /** Thrown for input that cannot be parsed or planned at all. */ declare class ImportError extends Error { constructor(message: string); } /** * Detect the input format and flatten it to rows. * * Accepts a JSON array of objects, one JSON object per line (JSONL), a single * JSON object, or a `{ namespace: rows[] }` map — the last being the shape a * query result already has, so query output can be pasted straight back in. * * The whole text is parsed as JSON **first**, so JSONL whose opening line is * itself a complete JSON object resolves deterministically as JSONL rather than * depending on which check happens to run first. * * @param ns Target namespace for row-shaped input. A `{ namespace: rows }` map * carries its own namespaces and ignores this. * @throws {ImportError} for a non-object document or a malformed JSONL line * (the message names the line number). */ declare function parseImport(text: string, ns?: string): ImportParseResult; /** * Build one upsert op per row. * * Problems are reported per row rather than failing the whole input, so a caller * can drop the bad rows and import the rest. Rows are rejected when they carry * no usable id, are not objects, target a platform-owned (`$`-prefixed) * namespace, would write nothing, or are individually too large for any single * transaction. * * Server-maintained `$` fields (`$createdAt`, `$updatedBy`, …) are stripped * rather than rejected — query output carries them, and re-importing it is the * common case — and reported in {@link ImportBuildResult.ignored}. */ declare function buildImportOps(sources: ImportSource[], opts: ImportOptions): ImportBuildResult; /** * Pack ops into transaction-sized chunks, respecting both the op-count cap and * the byte budget. Each op is measured once — re-costing a candidate chunk on * every addition would be quadratic on a large import. * * @throws {ImportError} if a single op exceeds the byte budget and so could * never be sent. {@link buildImportOps} already rejects such rows, so this * guards direct callers, where emitting an empty chunk would loop forever. */ declare function planImportChunks(ops: UpdateOp[], limits?: { maxOps?: number; maxBytes?: number; }): ImportChunk[]; /** * The mutation id for one chunk of an import run. * * Mutation ids are unique per writer, so a bare `import:0` would collide with — * and be silently swallowed as a duplicate of — the *previous* import's first * chunk, writing nothing. Scoping to a run id fixes that, and makes retries * safe: re-sending a run's chunks skips whatever already committed. * * Mint `runId` once when the import starts and reuse it across retries. If the * input changes, mint a **new** one: chunk boundaries move, so the same id would * name different ops than the chunk already committed under it. */ declare function importMutationId(runId: string, chunkIndex: number): string; export { IMPORT_CHUNK_MAX_BYTES, type ImportBuildResult, type ImportChunk, ImportError, type ImportFormat, type ImportIdMode, type ImportOptions, type ImportParseResult, type ImportRejection, type ImportSource, MAX_TRANSACTION_OPS, type UpdateOp, buildImportOps, importMutationId, parseImport, planImportChunks };