/** * The SPARQL this layer writes — one statement, and one use for it: the field an * index declares, written on the index document's own subject at creation. * * There is NO delete builder, and there must not be one: an index never removes * an entry, and the surest way to keep that true is for this package to be unable * to express a removal at all. * * `@ng-eventually/polyfill` has escaping helpers of its own but does NOT publish * them — `contract_polyfill-surface` lists no `escapeIri` / `escapeLiteral` — so * this package carries its own rather than reaching into that package's internals. * * Pure string builders, on purpose: their exact output is asserted in the unit * tests, and they are executed against a broker only through `polyfillPort`. */ /** Characters that would close an IRI written between angle brackets. */ const IRI_DELIMITERS = new Set(['"', "<", ">", "\\", "^", "`", "{", "|", "}"]); /** True when the character may not appear inside `<...>`: a control, a space, or a delimiter. */ function breaksIri(character: string): boolean { const code = character.codePointAt(0); if (code === undefined) return false; return code <= 0x20 || code === 0x7f || IRI_DELIMITERS.has(character); } /** True when the whole string can be written between angle brackets as-is. */ export function isIriSafe(value: string): boolean { for (const character of value) { if (breaksIri(character)) return false; } return true; } /** Neutralises anything that could close a SPARQL string literal. */ export function escapeLiteral(value: string): string { return value .replace(/\\/g, "\\\\") .replace(/"/g, '\\"') .replace(/\n/g, "\\n") .replace(/\r/g, "\\r") .replace(/\t/g, "\\t"); } /** * Percent-encodes anything that could close an IRI written between angle * brackets. Printable non-ASCII is left alone — it is legal in an IRI, and * encoding it would corrupt it. */ export function escapeIri(value: string): string { let out = ""; for (const character of value) { if (!breaksIri(character)) { out += character; continue; } const code = character.codePointAt(0) ?? 0; out += `%${code.toString(16).toUpperCase().padStart(2, "0")}`; } return out; } /** * One triple, for the ANCHORED default graph. The only statement this package * ever writes, and it writes it once per index. * * No `GRAPH <…>` wrapper, and the document is not a parameter at all: it is named * once, as `docs.sparqlUpdate`'s anchor, which already scopes the write to that * repo's default graph. Naming it twice would be two chances to disagree. * * This is the polyfill's own shape, and the alignment is deliberate: it calls the * no-GRAPH form "the CANONICAL, always-safe shape and the one the anchored * default-graph read queries" (`src/surface/inbox.ts`). The anchored * `GRAPH ` form this package wrote until now ALSO round-trips — the * polyfill's own e2e harness verified it resolves to the same repo graph, with no * phantom graph — so this is not a bug being fixed. It is two layers writing the * same kind of data the same way, instead of leaving someone to work out later * whether the difference meant something. */ export function buildInsertTriple(subject: string, predicate: string, value: string): string { return ( `INSERT DATA { <${escapeIri(subject)}> <${escapeIri(predicate)}> ` + `"${escapeLiteral(value)}" }` ); }