2026-04-21 14:36:02 +08:00
|
|
|
import type { Schema, ReverseReferenceSchema } from "../types.js";
|
2026-04-21 13:55:47 +08:00
|
|
|
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.
|
2026-04-21 14:36:02 +08:00
|
|
|
* Format: # type TypeName = schema
|
2026-04-21 13:55:47 +08:00
|
|
|
* 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();
|
|
|
|
|
|
2026-04-21 14:36:02 +08:00
|
|
|
// Match pattern: type TypeName = schema
|
|
|
|
|
const match = content.match(/^type\s+([A-Z][a-zA-Z0-9]*)\s*=\s*(.+)$/);
|
2026-04-21 13:55:47 +08:00
|
|
|
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, string>,
|
|
|
|
|
): 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, string>,
|
|
|
|
|
): 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, string>,
|
|
|
|
|
): string {
|
|
|
|
|
// Check if the entire string is a type name
|
|
|
|
|
const expanded = expandTypeName(schemaString.trim(), declaredTypes);
|
|
|
|
|
if (expanded !== null) {
|
|
|
|
|
return expanded;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 16:37:54 +08:00
|
|
|
// Handle array suffix: TypeName[] or [tuple][]
|
|
|
|
|
const trimmed = schemaString.trim();
|
|
|
|
|
if (trimmed.endsWith("[]")) {
|
|
|
|
|
const inner = trimmed.slice(0, -2);
|
|
|
|
|
const expandedInner = expandSchemaInString(inner, declaredTypes);
|
|
|
|
|
return `${expandedInner}[]`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 13:55:47 +08:00
|
|
|
// 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<string, Schema>,
|
|
|
|
|
): 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<string, Schema>,
|
|
|
|
|
): Schema {
|
|
|
|
|
const schema = parseSchema(schemaString.trim());
|
|
|
|
|
return resolveTypeReferences(schema, declaredTypes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Parse a reverse reference declaration from a comment line.
|
2026-04-21 14:36:02 +08:00
|
|
|
* Format: # inject fieldName = ~tableName(foreignKey)
|
2026-04-21 13:55:47 +08:00
|
|
|
* 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();
|
|
|
|
|
|
2026-04-21 14:36:02 +08:00
|
|
|
// Match pattern: inject fieldName = ~tableName(foreignKey)
|
|
|
|
|
const match = content.match(/^inject\s+(\w+)\s*=\s*~(\w+)\((\w+)\)(\?)?$/);
|
2026-04-21 13:55:47 +08:00
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
}
|