@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
161 lines • 6.64 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 { asArray, asObject, pickString as pickStringOrNull, stringOf as strOf, } from "../../utils/records.js";
import { formatSegment } from "./pathing.js";
import { blocksToMarkdown } from "./render.js";
function boolOf(record, key) {
const value = record[key];
return typeof value === 'boolean' ? value : false;
}
function joinTitleFragments(fragments) {
let out = '';
for (const fragment of fragments) {
const obj = asObject(fragment);
const text = pickStringOrNull(obj, 'plain_text');
if (text !== null)
out += text;
}
return out;
}
export function extractTitle(page) {
const properties = asObject(page.properties);
const titleProp = asObject(properties.title);
const titleFragments = asArray(titleProp.title);
if (titleFragments.length > 0) {
const joined = joinTitleFragments(titleFragments);
if (joined !== '')
return joined;
}
const nameProp = asObject(properties.Name);
const nameFragments = asArray(nameProp.title);
if (nameFragments.length > 0) {
const joined = joinTitleFragments(nameFragments);
if (joined !== '')
return joined;
}
return 'untitled';
}
export function pageSegmentName(page) {
return formatSegment({ id: strOf(page, 'id'), title: pageContentTitle(page) });
}
function pageContentTitle(page) {
const properties = asObject(page.properties);
for (const value of Object.values(properties)) {
const prop = asObject(value);
if (strOf(prop, 'type') === 'title') {
return joinTitleFragments(asArray(prop.title));
}
}
return '';
}
function extractDatabaseTitle(database) {
const titleFragments = asArray(database.title);
if (titleFragments.length > 0) {
const joined = joinTitleFragments(titleFragments);
if (joined !== '')
return joined;
}
return 'untitled';
}
export function databaseSegmentName(database) {
return formatSegment({ id: strOf(database, 'id'), title: extractDatabaseTitle(database) });
}
// A data source arrives either as the full object (rich-text `title`) or as a
// stub under a database's `data_sources` (plain `name`). Both name the same
// thing, so both must render the same directory.
function extractDataSourceTitle(dataSource) {
const name = pickStringOrNull(dataSource, 'name');
if (name !== null && name !== '')
return name;
return extractDatabaseTitle(dataSource);
}
export function dataSourceSegmentName(dataSource) {
return formatSegment({ id: strOf(dataSource, 'id'), title: extractDataSourceTitle(dataSource) });
}
// A database row's cells are its `properties`, and they are the reason the row
// exists, so they belong in the file rather than only in a `datasources query`.
// Kept as Notion's own property objects for the same reason `blocks` is: the
// schema they answer to is rendered one level up, in data_source.json's
// `properties`.
function pageFields(page) {
const parent = asObject(page.parent);
const parentType = strOf(parent, 'type');
const rawParentId = parent[parentType];
const parentId = typeof rawParentId === 'string' ? rawParentId : '';
return {
page_id: strOf(page, 'id'),
title: pageContentTitle(page),
url: strOf(page, 'url'),
created_time: strOf(page, 'created_time'),
last_edited_time: strOf(page, 'last_edited_time'),
parent_type: parentType,
parent_id: parentId,
archived: boolOf(page, 'archived'),
created_by: strOf(asObject(page.created_by), 'id'),
last_edited_by: strOf(asObject(page.last_edited_by), 'id'),
properties: asObject(page.properties),
};
}
export function normalizePage(page, blocks) {
const contentBlocks = blocks.filter((block) => {
const type = strOf(block, 'type');
return type !== 'child_page' && type !== 'child_database';
});
return {
...pageFields(page),
markdown: blocksToMarkdown(contentBlocks),
blocks: contentBlocks,
};
}
/**
* One line of a data source's `rows.jsonl`: the row's `page.json` without
* the body, which a query does not carry, and with the path of that
* `page.json` below the data source. The rows are not listed as
* directories, so the line is where a row's directory name is found.
* Mirrors `normalize_row` in `mirage/core/notion/normalize.py`.
*/
export function normalizeRow(page) {
const { page_id, title, ...rest } = pageFields(page);
return { page_id, title, path: `${pageSegmentName(page)}/page.json`, ...rest };
}
export function normalizeDatabase(database) {
const archived = database.archived !== undefined ? boolOf(database, 'archived') : boolOf(database, 'in_trash');
return {
database_id: strOf(database, 'id'),
title: extractDatabaseTitle(database),
url: pickStringOrNull(database, 'url'),
created_time: pickStringOrNull(database, 'created_time'),
last_edited_time: pickStringOrNull(database, 'last_edited_time'),
parent: asObject(database.parent),
archived,
is_inline: boolOf(database, 'is_inline'),
data_sources: asArray(database.data_sources),
};
}
export function normalizeDataSource(dataSource) {
const archived = dataSource.archived !== undefined
? boolOf(dataSource, 'archived')
: boolOf(dataSource, 'in_trash');
return {
data_source_id: strOf(dataSource, 'id'),
database_id: strOf(asObject(dataSource.parent), 'database_id'),
title: extractDataSourceTitle(dataSource),
created_time: pickStringOrNull(dataSource, 'created_time'),
last_edited_time: pickStringOrNull(dataSource, 'last_edited_time'),
database_parent: asObject(dataSource.database_parent),
archived,
properties: asObject(dataSource.properties),
};
}
//# sourceMappingURL=normalize.js.map