feat(ci): add Forgejo Actions workflow for auto-deploy on LinkML schema changes
Some checks are pending
Deploy Frontend / build-and-deploy (push) Waiting to run
Some checks are pending
Deploy Frontend / build-and-deploy (push) Waiting to run
Infrastructure changes to enable automatic frontend deployment when schemas change: - Add .forgejo/workflows/deploy-frontend.yml workflow triggered by: - Changes to frontend/** or schemas/20251121/linkml/** - Manual workflow dispatch - Rewrite generate-schema-manifest.cjs to properly scan all schema directories - Recursively scans classes, enums, slots, modules directories - Uses singular category names (class, enum, slot) matching TypeScript types - Includes all 4 main schemas at root level - Skips archive directories and backup files - Update schema-loader.ts to match new manifest format - Add SchemaCategory interface - Update SchemaManifest to use categories as array - Add flattenCategories() helper function - Add getSchemaCategories() and getSchemaCategoriesSync() functions The workflow builds frontend with updated manifest and deploys to bronhouder.nl
This commit is contained in:
parent
329b341bb1
commit
0f7fbf1ca0
4 changed files with 3340 additions and 2950 deletions
116
.forgejo/workflows/deploy-frontend.yml
Normal file
116
.forgejo/workflows/deploy-frontend.yml
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# GLAM Frontend Deployment
|
||||
# Automatically builds and deploys frontend to bronhouder.nl when changes are pushed
|
||||
#
|
||||
# Triggered by:
|
||||
# - Any changes to frontend/ directory
|
||||
# - Any changes to schemas/20251121/linkml/ directory (LinkML schema updates)
|
||||
# - Manual workflow dispatch
|
||||
#
|
||||
# This workflow:
|
||||
# 1. Syncs LinkML schemas to frontend/public
|
||||
# 2. Regenerates the schema manifest (for LinkML viewer)
|
||||
# 3. Builds the frontend (Vite/React)
|
||||
# 4. Deploys to server via rsync
|
||||
|
||||
name: Deploy Frontend
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
- 'schemas/20251121/linkml/**'
|
||||
- '.forgejo/workflows/deploy-frontend.yml'
|
||||
|
||||
# Allow manual trigger
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NODE_VERSION: '20'
|
||||
SERVER_IP: '91.98.224.44'
|
||||
SERVER_USER: 'root'
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: frontend
|
||||
run: npm ci
|
||||
|
||||
- name: Sync LinkML schemas to frontend
|
||||
working-directory: frontend
|
||||
run: npm run sync-schemas
|
||||
|
||||
- name: Generate schema manifest
|
||||
working-directory: frontend
|
||||
run: npm run generate-manifest
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: frontend
|
||||
run: npm run build
|
||||
env:
|
||||
VITE_OXIGRAPH_URL: https://bronhouder.nl
|
||||
|
||||
- name: Setup SSH key
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
ssh-keyscan -H ${{ env.SERVER_IP }} >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||
|
||||
- name: Deploy frontend to server
|
||||
run: |
|
||||
echo "Deploying frontend build to server..."
|
||||
rsync -avz --progress --delete \
|
||||
-e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no" \
|
||||
frontend/dist/ \
|
||||
${{ env.SERVER_USER }}@${{ env.SERVER_IP }}:/var/www/glam-frontend/
|
||||
|
||||
- name: Verify deployment
|
||||
run: |
|
||||
echo "Verifying deployment..."
|
||||
|
||||
# Check that Caddy is serving the frontend
|
||||
HTTP_STATUS=$(curl -s -o /dev/null -w '%{http_code}' https://bronhouder.nl/)
|
||||
|
||||
if [ "$HTTP_STATUS" = "200" ]; then
|
||||
echo "Frontend deployment verified: OK (HTTP $HTTP_STATUS)"
|
||||
else
|
||||
echo "Warning: Frontend returned HTTP $HTTP_STATUS"
|
||||
fi
|
||||
|
||||
# Check manifest is accessible
|
||||
MANIFEST_STATUS=$(curl -s -o /dev/null -w '%{http_code}' https://bronhouder.nl/schemas/20251121/linkml/manifest.json)
|
||||
|
||||
if [ "$MANIFEST_STATUS" = "200" ]; then
|
||||
echo "Schema manifest accessible: OK"
|
||||
# Show file count from manifest
|
||||
curl -s https://bronhouder.nl/schemas/20251121/linkml/manifest.json | jq '.totalFiles' | xargs -I {} echo "Total schema files: {}"
|
||||
else
|
||||
echo "Warning: Schema manifest returned HTTP $MANIFEST_STATUS"
|
||||
fi
|
||||
|
||||
- name: Deployment summary
|
||||
run: |
|
||||
echo "============================================"
|
||||
echo " Frontend Deployment Complete!"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "Server: ${{ env.SERVER_IP }}"
|
||||
echo "Frontend URL: https://bronhouder.nl/"
|
||||
echo "LinkML Viewer: https://bronhouder.nl/linkml"
|
||||
echo "Schema Manifest: https://bronhouder.nl/schemas/20251121/linkml/manifest.json"
|
||||
echo ""
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,19 +1,62 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generate a manifest of LinkML schema files for the frontend
|
||||
* This script scans the schemas directory and creates a JSON manifest
|
||||
* that the frontend can use to dynamically load schema files.
|
||||
* LinkML Schema Manifest Generator
|
||||
*
|
||||
* Run: node scripts/generate-schema-manifest.js
|
||||
* Generates a manifest.json file for the frontend to dynamically load
|
||||
* schema files from the public/schemas directory.
|
||||
*
|
||||
* This script is run as part of the build process:
|
||||
* pnpm run generate-manifest
|
||||
*
|
||||
* It scans the synced schema files (after sync-schemas) and creates
|
||||
* a structured manifest that the LinkML viewer can consume.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SCHEMAS_DIR = path.join(__dirname, '../public/schemas/20251121/linkml');
|
||||
const OUTPUT_FILE = path.join(__dirname, '../public/schemas/20251121/linkml/manifest.json');
|
||||
const OUTPUT_FILE = path.join(SCHEMAS_DIR, 'manifest.json');
|
||||
|
||||
function scanDirectory(dir, category) {
|
||||
// Category configuration
|
||||
const CATEGORIES = [
|
||||
{
|
||||
name: 'main',
|
||||
displayName: 'Main Schemas',
|
||||
scan: false, // Main schemas are at root level
|
||||
scanPath: null
|
||||
},
|
||||
{
|
||||
name: 'class',
|
||||
displayName: 'Classes',
|
||||
scan: true,
|
||||
scanPath: 'modules/classes'
|
||||
},
|
||||
{
|
||||
name: 'enum',
|
||||
displayName: 'Enumerations',
|
||||
scan: true,
|
||||
scanPath: 'modules/enums'
|
||||
},
|
||||
{
|
||||
name: 'slot',
|
||||
displayName: 'Slots',
|
||||
scan: true,
|
||||
scanPath: 'modules/slots'
|
||||
},
|
||||
{
|
||||
name: 'module',
|
||||
displayName: 'Modules',
|
||||
scan: true,
|
||||
scanPath: 'modules',
|
||||
excludeSubdirs: ['classes', 'enums', 'slots', 'archive']
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Recursively scan a directory for YAML files
|
||||
*/
|
||||
function scanDirectory(dir, category, relativePath = '', excludeSubdirs = []) {
|
||||
const files = [];
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
|
|
@ -21,14 +64,33 @@ function scanDirectory(dir, category) {
|
|||
return files;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(dir);
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.endsWith('.yaml') && !entry.endsWith('.bak.yaml') && !entry.includes('.bak')) {
|
||||
const name = entry.replace('.yaml', '');
|
||||
// Skip archive directories and backup files
|
||||
if (entry.name === 'archive' || entry.name.startsWith('archive_')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// Skip excluded subdirectories
|
||||
if (excludeSubdirs.includes(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
// Recurse into subdirectories
|
||||
const subPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
||||
files.push(...scanDirectory(
|
||||
path.join(dir, entry.name),
|
||||
category,
|
||||
subPath,
|
||||
[] // Don't propagate excludeSubdirs to recursive calls
|
||||
));
|
||||
} else if (entry.isFile() && entry.name.endsWith('.yaml') && !entry.name.includes('.bak')) {
|
||||
const name = entry.name.replace('.yaml', '');
|
||||
const filePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
||||
files.push({
|
||||
name,
|
||||
path: `modules/${category}/${entry}`,
|
||||
path: filePath,
|
||||
category
|
||||
});
|
||||
}
|
||||
|
|
@ -37,47 +99,95 @@ function scanDirectory(dir, category) {
|
|||
return files.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan root directory for main schema files
|
||||
*/
|
||||
function scanMainSchemas() {
|
||||
const files = [];
|
||||
|
||||
if (!fs.existsSync(SCHEMAS_DIR)) {
|
||||
console.warn(`Schema directory not found: ${SCHEMAS_DIR}`);
|
||||
return files;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(SCHEMAS_DIR, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile() && entry.name.endsWith('.yaml') && !entry.name.includes('.bak')) {
|
||||
const name = entry.name.replace('.yaml', '');
|
||||
files.push({
|
||||
name,
|
||||
path: entry.name,
|
||||
category: 'main'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return files.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the manifest
|
||||
*/
|
||||
function generateManifest() {
|
||||
console.log(`Scanning LinkML schemas in: ${SCHEMAS_DIR}`);
|
||||
|
||||
const categories = [];
|
||||
let totalFiles = 0;
|
||||
const categoryCounts = {};
|
||||
|
||||
for (const catConfig of CATEGORIES) {
|
||||
let files;
|
||||
|
||||
if (catConfig.name === 'main') {
|
||||
// Special handling for main schemas at root level
|
||||
files = scanMainSchemas();
|
||||
} else if (catConfig.scan && catConfig.scanPath) {
|
||||
const scanDir = path.join(SCHEMAS_DIR, catConfig.scanPath);
|
||||
files = scanDirectory(
|
||||
scanDir,
|
||||
catConfig.name,
|
||||
catConfig.scanPath,
|
||||
catConfig.excludeSubdirs || []
|
||||
);
|
||||
} else {
|
||||
files = [];
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
categories.push({
|
||||
name: catConfig.name,
|
||||
displayName: catConfig.displayName,
|
||||
files
|
||||
});
|
||||
categoryCounts[catConfig.name] = files.length;
|
||||
totalFiles += files.length;
|
||||
}
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
generated: new Date().toISOString(),
|
||||
version: '1.0.0',
|
||||
categories: [
|
||||
{
|
||||
name: 'main',
|
||||
displayName: 'Main Schema',
|
||||
files: [{
|
||||
name: 'Heritage Custodian Ontology',
|
||||
path: '01_custodian_name_modular.yaml',
|
||||
category: 'main'
|
||||
}]
|
||||
},
|
||||
{
|
||||
name: 'class',
|
||||
displayName: 'Classes',
|
||||
files: scanDirectory(path.join(SCHEMAS_DIR, 'modules/classes'), 'classes')
|
||||
},
|
||||
{
|
||||
name: 'enum',
|
||||
displayName: 'Enumerations',
|
||||
files: scanDirectory(path.join(SCHEMAS_DIR, 'modules/enums'), 'enums')
|
||||
},
|
||||
{
|
||||
name: 'slot',
|
||||
displayName: 'Slots',
|
||||
files: scanDirectory(path.join(SCHEMAS_DIR, 'modules/slots'), 'slots')
|
||||
}
|
||||
]
|
||||
schemaRoot: '/schemas/20251121/linkml',
|
||||
totalFiles,
|
||||
categoryCounts,
|
||||
categories
|
||||
};
|
||||
|
||||
// Ensure output directory exists
|
||||
const outputDir = path.dirname(OUTPUT_FILE);
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write manifest
|
||||
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(manifest, null, 2));
|
||||
|
||||
// Print summary
|
||||
console.log('Schema manifest generated:');
|
||||
for (const cat of manifest.categories) {
|
||||
console.log(` ${cat.displayName}: ${cat.files.length} files`);
|
||||
console.log(`Generated manifest with ${totalFiles} schema files`);
|
||||
for (const cat of categories) {
|
||||
console.log(` - ${cat.displayName}: ${cat.files.length}`);
|
||||
}
|
||||
console.log(`\nOutput: ${OUTPUT_FILE}`);
|
||||
console.log(`Output: ${OUTPUT_FILE}`);
|
||||
}
|
||||
|
||||
generateManifest();
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
* LinkML Schema Loader
|
||||
*
|
||||
* Utilities for loading and parsing LinkML schema files.
|
||||
*
|
||||
* DYNAMIC MANIFEST LOADING:
|
||||
* Instead of hardcoding schema files, this module fetches a manifest.json
|
||||
* that is auto-generated at build time by scripts/generate-linkml-manifest.js
|
||||
*/
|
||||
|
||||
import yaml from 'js-yaml';
|
||||
|
|
@ -68,52 +72,106 @@ export interface SchemaFile {
|
|||
category: 'main' | 'class' | 'slot' | 'enum' | 'module';
|
||||
}
|
||||
|
||||
/**
|
||||
* List of all LinkML schema files organized by category
|
||||
*/
|
||||
export const SCHEMA_FILES: SchemaFile[] = [
|
||||
// Main schema
|
||||
{ name: '01_custodian_name_modular', path: '01_custodian_name_modular.yaml', category: 'main' },
|
||||
|
||||
// Classes
|
||||
{ name: 'Custodian', path: 'modules/classes/Custodian.yaml', category: 'class' },
|
||||
{ name: 'CustodianName', path: 'modules/classes/CustodianName.yaml', category: 'class' },
|
||||
{ name: 'CustodianObservation', path: 'modules/classes/CustodianObservation.yaml', category: 'class' },
|
||||
{ name: 'CustodianLegalStatus', path: 'modules/classes/CustodianLegalStatus.yaml', category: 'class' },
|
||||
{ name: 'CustodianPlace', path: 'modules/classes/CustodianPlace.yaml', category: 'class' },
|
||||
{ name: 'CustodianCollection', path: 'modules/classes/CustodianCollection.yaml', category: 'class' },
|
||||
{ name: 'CustodianType', path: 'modules/classes/CustodianType.yaml', category: 'class' },
|
||||
{ name: 'ArchiveOrganizationType', path: 'modules/classes/ArchiveOrganizationType.yaml', category: 'class' },
|
||||
{ name: 'MuseumType', path: 'modules/classes/MuseumType.yaml', category: 'class' },
|
||||
{ name: 'LibraryType', path: 'modules/classes/LibraryType.yaml', category: 'class' },
|
||||
{ name: 'GalleryType', path: 'modules/classes/GalleryType.yaml', category: 'class' },
|
||||
{ name: 'DigitalPlatform', path: 'modules/classes/DigitalPlatform.yaml', category: 'class' },
|
||||
{ name: 'OrganizationalStructure', path: 'modules/classes/OrganizationalStructure.yaml', category: 'class' },
|
||||
{ name: 'OrganizationalChangeEvent', path: 'modules/classes/OrganizationalChangeEvent.yaml', category: 'class' },
|
||||
{ name: 'PersonObservation', path: 'modules/classes/PersonObservation.yaml', category: 'class' },
|
||||
{ name: 'Identifier', path: 'modules/classes/Identifier.yaml', category: 'class' },
|
||||
{ name: 'ReconstructionActivity', path: 'modules/classes/ReconstructionActivity.yaml', category: 'class' },
|
||||
{ name: 'SourceDocument', path: 'modules/classes/SourceDocument.yaml', category: 'class' },
|
||||
{ name: 'TimeSpan', path: 'modules/classes/TimeSpan.yaml', category: 'class' },
|
||||
{ name: 'EncompassingBody', path: 'modules/classes/EncompassingBody.yaml', category: 'class' },
|
||||
{ name: 'Country', path: 'modules/classes/Country.yaml', category: 'class' },
|
||||
{ name: 'Subregion', path: 'modules/classes/Subregion.yaml', category: 'class' },
|
||||
{ name: 'Settlement', path: 'modules/classes/Settlement.yaml', category: 'class' },
|
||||
{ name: 'AnnotationMotivationType', path: 'modules/classes/AnnotationMotivationType.yaml', category: 'class' },
|
||||
{ name: 'AnnotationMotivationTypes', path: 'modules/classes/AnnotationMotivationTypes.yaml', category: 'class' },
|
||||
|
||||
// Enums
|
||||
{ name: 'CustodianPrimaryTypeEnum', path: 'modules/enums/CustodianPrimaryTypeEnum.yaml', category: 'enum' },
|
||||
{ name: 'LegalStatusEnum', path: 'modules/enums/LegalStatusEnum.yaml', category: 'enum' },
|
||||
{ name: 'PlaceSpecificityEnum', path: 'modules/enums/PlaceSpecificityEnum.yaml', category: 'enum' },
|
||||
{ name: 'OrganizationalUnitTypeEnum', path: 'modules/enums/OrganizationalUnitTypeEnum.yaml', category: 'enum' },
|
||||
{ name: 'OrganizationalChangeEventTypeEnum', path: 'modules/enums/OrganizationalChangeEventTypeEnum.yaml', category: 'enum' },
|
||||
{ name: 'StaffRoleTypeEnum', path: 'modules/enums/StaffRoleTypeEnum.yaml', category: 'enum' },
|
||||
{ name: 'FeatureTypeEnum', path: 'modules/enums/FeatureTypeEnum.yaml', category: 'enum' },
|
||||
{ name: 'EncompassingBodyTypeEnum', path: 'modules/enums/EncompassingBodyTypeEnum.yaml', category: 'enum' },
|
||||
];
|
||||
export interface SchemaCategory {
|
||||
name: string;
|
||||
displayName: string;
|
||||
files: SchemaFile[];
|
||||
}
|
||||
|
||||
export interface SchemaManifest {
|
||||
generated: string;
|
||||
schemaRoot: string;
|
||||
totalFiles: number;
|
||||
categoryCounts: {
|
||||
main: number;
|
||||
class: number;
|
||||
enum: number;
|
||||
slot: number;
|
||||
module: number;
|
||||
};
|
||||
categories: SchemaCategory[];
|
||||
}
|
||||
|
||||
const SCHEMA_BASE_PATH = '/schemas/20251121/linkml';
|
||||
const MANIFEST_PATH = `${SCHEMA_BASE_PATH}/manifest.json`;
|
||||
|
||||
// Cache for the manifest
|
||||
let cachedManifest: SchemaManifest | null = null;
|
||||
let manifestLoadPromise: Promise<SchemaManifest | null> | null = null;
|
||||
|
||||
/**
|
||||
* Load the schema manifest (cached)
|
||||
*/
|
||||
export async function loadManifest(): Promise<SchemaManifest | null> {
|
||||
// Return cached manifest if available
|
||||
if (cachedManifest) {
|
||||
return cachedManifest;
|
||||
}
|
||||
|
||||
// If already loading, wait for that promise
|
||||
if (manifestLoadPromise) {
|
||||
return manifestLoadPromise;
|
||||
}
|
||||
|
||||
// Start loading
|
||||
manifestLoadPromise = (async () => {
|
||||
try {
|
||||
const response = await fetch(MANIFEST_PATH);
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to load manifest: ${response.status}`);
|
||||
return null;
|
||||
}
|
||||
cachedManifest = await response.json();
|
||||
return cachedManifest;
|
||||
} catch (error) {
|
||||
console.error('Error loading schema manifest:', error);
|
||||
return null;
|
||||
} finally {
|
||||
manifestLoadPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return manifestLoadPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to flatten categories into a single files array
|
||||
*/
|
||||
function flattenCategories(manifest: SchemaManifest | null): SchemaFile[] {
|
||||
if (!manifest) return [];
|
||||
return manifest.categories.flatMap(cat => cat.files);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all schema files (from manifest)
|
||||
*/
|
||||
export async function getSchemaFiles(): Promise<SchemaFile[]> {
|
||||
const manifest = await loadManifest();
|
||||
return flattenCategories(manifest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get schema files synchronously (returns empty if not yet loaded)
|
||||
* Useful for initial renders before async data is available
|
||||
*/
|
||||
export function getSchemaFilesSync(): SchemaFile[] {
|
||||
return flattenCategories(cachedManifest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all categories with their files
|
||||
*/
|
||||
export async function getSchemaCategories(): Promise<SchemaCategory[]> {
|
||||
const manifest = await loadManifest();
|
||||
return manifest?.categories ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all categories synchronously (returns empty if not yet loaded)
|
||||
*/
|
||||
export function getSchemaCategoriesSync(): SchemaCategory[] {
|
||||
return cachedManifest?.categories ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a LinkML schema file by path
|
||||
|
|
@ -154,8 +212,16 @@ export async function loadSchemaRaw(schemaPath: string): Promise<string | null>
|
|||
/**
|
||||
* Get all schema files by category
|
||||
*/
|
||||
export function getSchemasByCategory(category: SchemaFile['category']): SchemaFile[] {
|
||||
return SCHEMA_FILES.filter(s => s.category === category);
|
||||
export async function getSchemasByCategory(category: SchemaFile['category']): Promise<SchemaFile[]> {
|
||||
const files = await getSchemaFiles();
|
||||
return files.filter(s => s.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get schemas by category synchronously (returns empty if manifest not loaded)
|
||||
*/
|
||||
export function getSchemasByCategorySync(category: SchemaFile['category']): SchemaFile[] {
|
||||
return getSchemaFilesSync().filter(s => s.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -179,6 +245,17 @@ export function getCategoryDisplayName(category: SchemaFile['category']): string
|
|||
return names[category];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get manifest metadata (generation time, counts, etc.)
|
||||
*/
|
||||
export async function getManifestMetadata(): Promise<Omit<SchemaManifest, 'categories'> | null> {
|
||||
const manifest = await loadManifest();
|
||||
if (!manifest) return null;
|
||||
|
||||
const { categories, ...metadata } = manifest;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract class information from a parsed schema
|
||||
*/
|
||||
|
|
@ -211,3 +288,26 @@ export function extractEnums(schema: LinkMLSchema): LinkMLEnum[] {
|
|||
name: key,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Search schema files by name (case-insensitive partial match)
|
||||
*/
|
||||
export async function searchSchemas(query: string): Promise<SchemaFile[]> {
|
||||
const files = await getSchemaFiles();
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return files.filter(f => f.name.toLowerCase().includes(lowerQuery));
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload the manifest (call early to reduce latency)
|
||||
*/
|
||||
export function preloadManifest(): void {
|
||||
loadManifest().catch(() => {
|
||||
// Silently ignore preload errors
|
||||
});
|
||||
}
|
||||
|
||||
// Legacy export for backwards compatibility
|
||||
// Components using SCHEMA_FILES directly should migrate to getSchemaFiles()
|
||||
/** @deprecated Use getSchemaFiles() instead */
|
||||
export const SCHEMA_FILES: SchemaFile[] = [];
|
||||
|
|
|
|||
Loading…
Reference in a new issue