@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
247 lines • 10.1 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { compareCodePoints } from "../../utils/sort.js";
import { valueText } from "../render/json.js";
import { groupName, rowStem } from "./naming.js";
import { fieldValue } from "./payload.js";
export const SCROLL_BATCH = 256;
/**
* The non-string JSON scalar a rendered group segment also spells, or null.
*
* A group value renders through `valueText`, so a boolean or a number lists
* as its compact JSON and the segment alone cannot say which type the
* payload holds. Only a spelling `valueText` would produce counts: `007`,
* `-0` and `1.50` are strings and nothing else.
*/
export function jsonScalar(text) {
let parsed;
try {
parsed = JSON.parse(text);
}
catch {
return null;
}
if (typeof parsed === 'boolean')
return parsed;
if (typeof parsed === 'number' && valueText(parsed) === text)
return parsed;
return null;
}
/**
* What one rendered group segment matches in the payload: the string itself
* always, and when the segment also spells a JSON scalar, that typed value
* too, so descending into the `true` or `1.5` directory the listing
* advertised finds the boolean or float points behind it. A number matches
* as a closed range, which Qdrant applies to integer and float payloads
* alike where `match` does not.
*/
export function condition(key, text) {
const asText = { key, match: { value: text } };
const scalar = jsonScalar(text);
if (scalar === null)
return asText;
const typed = typeof scalar === 'boolean'
? { key, match: { value: scalar } }
: { key, range: { gte: scalar, lte: scalar } };
return { should: [asText, typed] };
}
export function buildFilter(filters) {
const keys = Object.keys(filters);
if (keys.length === 0)
return undefined;
return { must: keys.map((key) => condition(key, filters[key] ?? '')) };
}
/** Keep points whose id starts with a literal name prefix. */
export function idPrefixTest(prefix) {
return (point) => String(point.id).startsWith(prefix);
}
/** Keep points whose payload value starts with a literal prefix. */
export function valuePrefixTest(column, prefix, basename = false) {
return (point) => {
const value = fieldValue(point.payload ?? {}, column);
if (value === null || value === undefined)
return false;
return groupName(value, basename).startsWith(prefix);
};
}
/** Keep the first point of every raw value that renders as one group name. */
export function exactNameTest(column, name, basename, seen) {
return (point) => {
const value = fieldValue(point.payload ?? {}, column);
if (value === null || value === undefined)
return false;
const raw = valueText(value);
if (seen.has(raw) || groupName(raw, basename) !== name)
return false;
seen.add(raw);
return true;
};
}
export function pointToRow(point, idField) {
return { ...point.payload, [idField]: point.id };
}
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
export function candidateIds(rowId) {
if (/^-?\d+$/.test(rowId))
return [Number.parseInt(rowId, 10)];
if (UUID_RE.test(rowId))
return [rowId];
return [];
}
async function scrollRaw(accessor, collection, filter, limit, keep) {
// Without a test the limit bounds the scroll, which is the ordinary capped
// listing. With one it bounds the MATCHES, because qdrant has no prefix
// condition for a point id or a keyword field: the only way to answer a
// glob for a row past the cap is to keep scrolling and test each page
// here. A glob is a targeted request, so it pays a scan of the collection
// where the plain listing pays one page.
const client = await accessor.client();
const points = [];
let offset = null;
while (points.length < limit) {
const res = (await client.scroll(collection, {
...(filter !== undefined ? { filter } : {}),
limit: keep !== undefined ? SCROLL_BATCH : Math.min(SCROLL_BATCH, limit - points.length),
offset,
with_payload: true,
with_vector: false,
}));
for (const item of keep === undefined ? res.points : res.points.filter(keep))
points.push(item);
const next = res.next_page_offset;
if (next === null || next === undefined)
break;
offset = next;
}
return points.slice(0, limit);
}
function isIndexRequired(err) {
if (typeof err !== 'object' || err === null)
return false;
const e = err;
if (e.status !== 400)
return false;
const text = `${JSON.stringify(e.data ?? '')} ${e.message ?? ''}`.toLowerCase();
return text.includes('index required');
}
async function ensureIndexes(accessor, collection) {
if (accessor.indexesEnsured.has(collection))
return;
const client = await accessor.client();
for (const field of accessor.config.groupBy) {
await client.createPayloadIndex(collection, { field_name: field, field_schema: 'keyword' });
}
accessor.indexesEnsured.add(collection);
}
async function scrollAll(accessor, collection, filters, limit, keep) {
const filter = buildFilter(filters);
if (filter === undefined)
return scrollRaw(accessor, collection, undefined, limit, keep);
try {
return await scrollRaw(accessor, collection, filter, limit, keep);
}
catch (err) {
if (!isIndexRequired(err))
throw err;
}
await ensureIndexes(accessor, collection);
return scrollRaw(accessor, collection, filter, limit, keep);
}
export async function listTables(accessor) {
const client = await accessor.client();
const res = (await client.getCollections());
return res.collections.map((c) => c.name).sort(compareCodePoints);
}
export async function tableExists(accessor, name) {
const client = await accessor.client();
const res = (await client.collectionExists(name));
return res.exists;
}
export async function distinctValues(accessor, table, column, filters, limit, prefix = '', basename = false) {
const keep = prefix === '' ? undefined : valuePrefixTest(column, prefix, basename);
const points = await scrollAll(accessor, table, filters, limit, keep);
const values = new Set();
for (const point of points) {
const value = fieldValue(point.payload ?? {}, column);
if (value !== null && value !== undefined)
values.add(valueText(value));
}
return [...values].sort(compareCodePoints);
}
/**
* The raw payload values one rendered group segment stands for.
*
* A basename drops the value's parents, so two sources can render as the
* same directory. Telling them apart is a question about every point under
* the parent group, not about the first `maxRows`: the scroll runs until it
* is exhausted or a second distinct value has rendered as `name`, whichever
* comes first. One value is the answer; two is a collision for the caller
* to refuse.
*/
export async function resolveGroup(accessor, table, column, filters, name, basename = false) {
const keep = exactNameTest(column, name, basename, new Set());
const points = await scrollAll(accessor, table, filters, 2, keep);
return points
.map((point) => valueText(fieldValue(point.payload ?? {}, column)))
.sort(compareCodePoints);
}
export async function rowsMatching(accessor, table, filters, limit, prefix = '') {
const config = accessor.config;
const keep = prefix === ''
? undefined
: config.nameField !== null
? (point) => rowStem(pointToRow(point, config.idField), config).startsWith(prefix)
: idPrefixTest(prefix);
const points = await scrollAll(accessor, table, filters, limit, keep);
return points.map((point) => pointToRow(point, config.idField));
}
export async function rowRecord(accessor, table, idField, rowId) {
const ids = candidateIds(rowId);
if (ids.length === 0)
return null;
const client = await accessor.client();
const found = (await client.retrieve(table, {
ids,
with_payload: true,
with_vector: false,
}));
return found[0] !== undefined ? pointToRow(found[0], idField) : null;
}
export async function searchRows(accessor, table, query, limit) {
return accessor.searches.get(JSON.stringify([table, query, limit]), async () => {
const config = accessor.config;
const client = await accessor.client();
// A caller-supplied `embed` vectorizes the query here, and cloud inference
// sends the text to a cluster that embeds it.
let vector;
if (config.embed !== null)
vector = await config.embed(query);
else if (config.cloudInference)
vector = { text: query, model: config.embeddingModel };
else {
throw new Error('the query needs a vector: pass embed, or set cloud_inference for a cluster with inference');
}
const res = (await client.query(table, {
query: vector,
limit,
with_payload: true,
}));
return res.points.map((point) => {
const row = pointToRow(point, config.idField);
row._score = point.score;
return row;
});
});
}
//# sourceMappingURL=query.js.map