86 lines
3.4 KiB
TypeScript
86 lines
3.4 KiB
TypeScript
/**
|
|
* 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 <plainNuri>` 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)}" }`
|
|
);
|
|
}
|