UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

161 lines • 6.64 kB
// ========= 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