import type { Schema, ReverseReferenceSchema } from "../types.js"; import { parseSchema } from "../parser.js"; export interface ReverseReferenceDeclaration { fieldName: string; tableName: string; foreignKey: string; isOptional: boolean; schema: ReverseReferenceSchema; } export interface TypeDeclaration { name: string; schema: Schema; } /** * Parse a type declaration from a comment line. * Format: # type TypeName = schema * Returns null if the line is not a type declaration. */ export function parseTypeDeclaration( line: string, commentChar: string = "#", ): { typeName: string; schemaString: string } | null { const trimmed = line.trim(); // Must start with the comment character if (!trimmed.startsWith(commentChar)) return null; const content = trimmed.slice(commentChar.length).trim(); // Match pattern: type TypeName = schema const match = content.match(/^type\s+([A-Z][a-zA-Z0-9]*)\s*=\s*(.+)$/); if (!match) return null; const [, typeName, schemaString] = match; return { typeName, schemaString }; } /** * Expand a type name to its schema by replacing the type name with its schema inline. * Returns the schema string with type names expanded, or null if not a type name. */ function expandTypeName( schemaString: string, declaredTypes: Map, ): string | null { const trimmed = schemaString.trim(); if (declaredTypes.has(trimmed)) { return declaredTypes.get(trimmed)!; } return null; } /** * Recursively expand all type name references in a schema string. * Handles unions, tuples, arrays, and nested structures. */ export function expandSchemaString( schemaString: string, declaredTypes: Map, ): string { let result = schemaString; // Keep expanding until no more changes (handles recursive dependencies) let prev = ""; while (prev !== result) { prev = result; result = expandSchemaInString(result, declaredTypes); } return result; } /** * Single pass of type name expansion in a schema string. */ function expandSchemaInString( schemaString: string, declaredTypes: Map, ): string { // Check if the entire string is a type name const expanded = expandTypeName(schemaString.trim(), declaredTypes); if (expanded !== null) { return expanded; } // Handle union types (recursively expand each member) if (schemaString.includes("|")) { // Split by | but respect quotes const parts = splitByToken(schemaString, "|"); if (parts.length > 1) { const expandedParts = parts.map((part) => expandSchemaInString(part.trim(), declaredTypes), ); return expandedParts.join(" | "); } } // Handle tuple/array syntax [el1; el2; ...] or [elements] // Check if it's a bracketed structure if (schemaString.startsWith("[") && schemaString.endsWith("]")) { const inner = schemaString.slice(1, -1); // Check if it's semicolon-separated (tuple syntax) if (inner.includes(";")) { const elements = splitByToken(inner, ";"); const expandedElements = elements.map((el) => expandSchemaInString(el.trim(), declaredTypes), ); return `[${expandedElements.join("; ")}]`; } // Otherwise it's a simple array, expand recursively return `[${expandSchemaInString(inner, declaredTypes)}]`; } // Check if it's a type name reference (only uppercase start to avoid conflicts with primitives) const typeNameMatch = schemaString.trim().match(/^[A-Z][a-zA-Z0-9]*$/); if (typeNameMatch) { const expanded = expandTypeName(schemaString.trim(), declaredTypes); if (expanded !== null) { return expanded; } } return schemaString; } /** * Split a string by a token, respecting quoted strings. */ function splitByToken(str: string, token: string): string[] { const result: string[] = []; let current = ""; let inQuote: string | null = null; for (let i = 0; i < str.length; i++) { const char = str[i]; if (inQuote) { if (char === inQuote && str[i - 1] !== "\\") { inQuote = null; } current += char; } else if (char === '"' || char === "'") { inQuote = char; current += char; } else if (char === token && inQuote === null) { result.push(current); current = ""; } else { current += char; } } if (current.length > 0 || str.endsWith(token)) { result.push(current); } return result; } /** * Resolve type name references within a schema using declared types. * For example, if "Trigger" is a declared type, references to "Trigger" in * other schemas will be replaced with the actual Trigger schema definition. */ export function resolveTypeReferences( schema: Schema, declaredTypes: Map, ): Schema { switch (schema.type) { case "union": return { type: "union", members: schema.members.map((m) => resolveTypeReferences(m, declaredTypes), ), }; case "tuple": return { type: "tuple", elements: schema.elements.map((el) => ({ name: el.name, schema: resolveTypeReferences(el.schema, declaredTypes), })), }; case "array": return { type: "array", element: resolveTypeReferences(schema.element, declaredTypes), }; case "reference": // Don't resolve references to other tables return schema; default: return schema; } } /** * Resolve type name references in a type declaration's schema string. * Called after all type names are known. */ export function resolveTypeDeclarationSchema( schemaString: string, declaredTypes: Map, ): Schema { const schema = parseSchema(schemaString.trim()); return resolveTypeReferences(schema, declaredTypes); } /** * Parse a reverse reference declaration from a comment line. * Format: # inject fieldName = ~tableName(foreignKey) * Returns null if the line is not a reverse reference declaration. */ export function parseReverseReferenceDeclaration( line: string, commentChar: string = "#", ): ReverseReferenceDeclaration | null { const trimmed = line.trim(); // Must start with the comment character if (!trimmed.startsWith(commentChar)) return null; const content = trimmed.slice(commentChar.length).trim(); // Match pattern: inject fieldName = ~tableName(foreignKey) const match = content.match(/^inject\s+(\w+)\s*=\s*~(\w+)\((\w+)\)(\?)?$/); if (!match) return null; const [, fieldName, tableName, foreignKey, optionalMark] = match; const isOptional = optionalMark === "?"; const schema: ReverseReferenceSchema = { type: "reverseReference", tableName, foreignKey, isOptional, }; return { fieldName, tableName, foreignKey, isOptional, schema, }; }