@happyvertical/smrt-core 0.36.4 → 0.36.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/collection.d.ts +9 -4
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +6 -0
- package/dist/collection.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/consumer-plugin/index.d.ts.map +1 -1
- package/dist/consumer-plugin/index.js +7 -3
- package/dist/consumer-plugin/index.js.map +1 -1
- package/dist/database.d.ts +3 -3
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js.map +1 -1
- package/dist/embeddings/provider.d.ts +14 -2
- package/dist/embeddings/provider.d.ts.map +1 -1
- package/dist/embeddings/provider.js +3 -3
- package/dist/embeddings/provider.js.map +1 -1
- package/dist/embeddings/storage.js.map +1 -1
- package/dist/errors.d.ts +22 -22
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +5 -4
- package/dist/errors.js.map +1 -1
- package/dist/knowledge.d.ts +12 -1
- package/dist/knowledge.d.ts.map +1 -1
- package/dist/knowledge.js.map +1 -1
- package/dist/lazy-config.d.ts.map +1 -1
- package/dist/lazy-config.js.map +1 -1
- package/dist/manifest/generator.d.ts.map +1 -1
- package/dist/manifest/generator.js +14 -12
- package/dist/manifest/generator.js.map +1 -1
- package/dist/manifest/manager.d.ts +3 -3
- package/dist/manifest/manager.d.ts.map +1 -1
- package/dist/manifest/manager.js.map +1 -1
- package/dist/manifest/manifest-loader.d.ts +3 -2
- package/dist/manifest/manifest-loader.d.ts.map +1 -1
- package/dist/manifest/manifest-loader.js +3 -2
- package/dist/manifest/manifest-loader.js.map +1 -1
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +13 -198
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +2 -2
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
- package/dist/manifest/test-manifest-stub.js +13 -198
- package/dist/manifest/test-manifest-stub.js.map +1 -1
- package/dist/manifest.json +13 -175
- package/dist/mcp-advisor/index.d.ts +1 -1
- package/dist/mcp-advisor/index.d.ts.map +1 -1
- package/dist/mcp-advisor/tools/get-object-config.d.ts.map +1 -1
- package/dist/mcp-advisor/types.d.ts +5 -5
- package/dist/mcp-advisor/types.d.ts.map +1 -1
- package/dist/migrations/differ.js.map +1 -1
- package/dist/registry/types.d.ts +21 -0
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +9 -0
- package/dist/registry.js.map +1 -1
- package/dist/scanner/manifest-generator.d.ts +13 -3
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +49 -16
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +60 -6
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/schema/code-generator.d.ts.map +1 -1
- package/dist/schema/ddl/base-strategy.d.ts +2 -2
- package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/base-strategy.js.map +1 -1
- package/dist/schema/ddl/sqlite-strategy.d.ts +1 -1
- package/dist/schema/ddl/sqlite-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/sqlite-strategy.js.map +1 -1
- package/dist/schema/ddl/types.d.ts +1 -1
- package/dist/schema/ddl/types.d.ts.map +1 -1
- package/dist/schema/generator.d.ts +27 -4
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +3 -2
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/override-system.d.ts +3 -3
- package/dist/schema/override-system.d.ts.map +1 -1
- package/dist/schema/schema-aggregator.d.ts.map +1 -1
- package/dist/schema/schema-manager.d.ts.map +1 -1
- package/dist/schema/schema-manager.js +12 -4
- package/dist/schema/schema-manager.js.map +1 -1
- package/dist/schema/types.d.ts +1 -1
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/utils.d.ts +6 -1
- package/dist/schema/utils.d.ts.map +1 -1
- package/dist/schema/utils.js +2 -1
- package/dist/schema/utils.js.map +1 -1
- package/dist/signals/sanitizer.d.ts +1 -1
- package/dist/signals/sanitizer.d.ts.map +1 -1
- package/dist/signals/sanitizer.js +12 -2
- package/dist/signals/sanitizer.js.map +1 -1
- package/dist/smrt-knowledge.json +14 -26
- package/dist/system/types.d.ts +2 -2
- package/dist/system/types.d.ts.map +1 -1
- package/dist/system-fields.d.ts +5 -43
- package/dist/system-fields.d.ts.map +1 -1
- package/dist/system-fields.js +2 -1
- package/dist/system-fields.js.map +1 -1
- package/dist/test-utils.d.ts +39 -13
- package/dist/test-utils.d.ts.map +1 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js.map +1 -1
- package/dist/utils/json.js.map +1 -1
- package/dist/utils.d.ts +16 -8
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +11 -7
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +4 -3
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/templates/default-ui.ts +20 -6
- package/package.json +10 -10
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.js","sources":["../../src/testing/database.ts"],"sourcesContent":["/**\n * Test database utilities for SMRT framework\n *\n * Provides `getTestDatabase()` which creates an in-memory database with all\n * registered SMRT object schemas. Uses the same schema generation logic as\n * the migration system to ensure consistency between test and production.\n *\n * @example\n * ```typescript\n * import { getTestDatabase } from '@happyvertical/smrt-core/testing';\n *\n * beforeEach(async () => {\n * const db = await getTestDatabase();\n * collection = await MyCollection.create({ db });\n * });\n * ```\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { getDatabase } from '@happyvertical/sql';\nimport {\n type CollectionRegistrationLookup,\n isCollectionRegistration,\n resolveCollectionItemClassName,\n resolveRelatedRegistration,\n} from '../registry/collection-resolution.js';\nimport { ObjectRegistry } from '../registry.js';\nimport { SchemaGenerator } from '../schema/generator.js';\nimport { ensureLegacySystemTableCompatibility } from '../system/compatibility.js';\nimport { ALL_SYSTEM_TABLES } from '../system/schema.js';\n\ntype TestDatabaseConnectionOptions = Parameters<typeof getDatabase>[0] & {\n __smrtSkipVitestSchemaPreparation?: boolean;\n};\n\nfunction resolveSTIBaseRegistration(className: string, stiBaseName: string) {\n const registered = ObjectRegistry.getClass(className);\n\n if (stiBaseName.includes(':')) {\n return ObjectRegistry.getClass(stiBaseName);\n }\n\n if (registered?.packageName) {\n const samePackageBase = ObjectRegistry.getClassInPackage(\n registered.packageName,\n stiBaseName,\n );\n if (samePackageBase) {\n return samePackageBase;\n }\n }\n\n return ObjectRegistry.getClass(stiBaseName);\n}\n\nfunction resolveSTIBaseLookupName(\n className: string,\n stiBaseName: string,\n): string {\n const stiBase = resolveSTIBaseRegistration(className, stiBaseName);\n return stiBase?.qualifiedName || stiBase?.name || stiBaseName;\n}\n\nfunction isSTIChild(className: string): boolean {\n const stiBaseName = ObjectRegistry.getSTIBase(className);\n if (!stiBaseName) {\n return false;\n }\n\n const registered = ObjectRegistry.getClass(className);\n const stiBase = resolveSTIBaseRegistration(className, stiBaseName);\n\n if (registered && stiBase) {\n return registered !== stiBase;\n }\n\n return stiBaseName !== className;\n}\n\ntype RegisteredSchemaClass = NonNullable<\n ReturnType<typeof ObjectRegistry.getClass>\n>;\n\nconst collectionRegistrationLookup: CollectionRegistrationLookup = {\n findClass: (className) => ObjectRegistry.getClass(className),\n findClassInPackage: (packageName, className) =>\n ObjectRegistry.getClassInPackage(packageName, className),\n getInheritanceChain: (className) =>\n ObjectRegistry.getInheritanceChain(className),\n};\n\nfunction resolveCollectionSchemaClassName(\n className: string,\n registered: RegisteredSchemaClass,\n): string {\n const itemClassName = resolveCollectionItemClassName(\n className,\n registered,\n collectionRegistrationLookup,\n );\n if (itemClassName) {\n const itemRegistration = resolveRelatedRegistration(\n itemClassName,\n registered,\n collectionRegistrationLookup,\n );\n const itemLookupName =\n itemRegistration?.qualifiedName ||\n itemRegistration?.name ||\n itemClassName;\n const stiBase = ObjectRegistry.getSTIBase(itemLookupName);\n return stiBase\n ? resolveSTIBaseLookupName(itemLookupName, stiBase)\n : itemLookupName;\n }\n\n const tableName =\n registered.schema?.tableName ||\n registered.config.tableName ||\n ObjectRegistry.getTableName(className);\n if (!tableName) {\n return className;\n }\n\n const collectionPackage = registered.packageName;\n\n for (const candidate of ObjectRegistry.getAllClasses().values()) {\n if (\n candidate === registered ||\n isCollectionRegistration(\n candidate.qualifiedName || candidate.name,\n candidate,\n collectionRegistrationLookup,\n )\n ) {\n continue;\n }\n\n const candidateTableName =\n candidate.schema?.tableName || candidate.config.tableName;\n if (candidateTableName !== tableName) {\n continue;\n }\n\n if (\n collectionPackage &&\n candidate.packageName &&\n candidate.packageName !== collectionPackage\n ) {\n continue;\n }\n\n const candidateLookupName = candidate.qualifiedName || candidate.name;\n const stiBase = ObjectRegistry.getSTIBase(candidateLookupName);\n return stiBase\n ? resolveSTIBaseLookupName(candidateLookupName, stiBase)\n : candidateLookupName;\n }\n\n return className;\n}\n\n/**\n * Options for creating a test database\n */\nexport interface TestDatabaseOptions {\n /**\n * Database type (default: 'sqlite')\n * - 'sqlite': SQLite database\n * - 'json': JSON adapter (stores data as JSON files with DuckDB for querying)\n */\n type?: 'sqlite' | 'json';\n\n /**\n * Database URL (default: ':memory:')\n * Use ':memory:' for in-memory databases (fastest for tests)\n * Or provide a file path for persistent test databases\n */\n url?: string;\n\n /**\n * Pre-existing database to initialize schemas in.\n * If provided, `type` and `url` are ignored.\n */\n db?: DatabaseInterface;\n\n /**\n * Specific classes to setup schemas for.\n * If not provided, sets up schemas for all registered classes.\n */\n classes?: string[];\n\n /**\n * Whether to include system tables (default: true)\n * System tables include _smrt_contexts, _smrt_migrations, etc.\n */\n includeSystemTables?: boolean;\n}\n\nfunction resolveRequestedSchemaClassName(className: string): string {\n const registered = ObjectRegistry.getClass(className);\n if (!registered) {\n return className;\n }\n\n if (\n !isCollectionRegistration(\n className,\n registered,\n collectionRegistrationLookup,\n )\n ) {\n const stiBase = ObjectRegistry.getSTIBase(className);\n return stiBase ? resolveSTIBaseLookupName(className, stiBase) : className;\n }\n\n return resolveCollectionSchemaClassName(className, registered);\n}\n\nfunction resolveRequestedSchemaClassNames(classNames: string[]): string[] {\n const resolved: string[] = [];\n const seen = new Set<string>();\n\n for (const className of classNames) {\n const schemaClassName = resolveRequestedSchemaClassName(className);\n if (seen.has(schemaClassName)) {\n continue;\n }\n\n seen.add(schemaClassName);\n resolved.push(schemaClassName);\n }\n\n return resolved;\n}\n\n/**\n * Creates an in-memory test database with schemas pre-created\n *\n * This utility is designed for testing. It creates an in-memory database\n * and initializes all registered SMRT object schemas using the **same\n * schema generation logic** as the production migration system.\n *\n * **Key features:**\n * - Uses `SchemaGenerator.generateSQL()` - the single source of truth for DDL\n * - Handles STI (Single Table Inheritance) correctly\n * - Creates system tables for framework functionality\n * - Safe for parallel test execution (each call creates isolated instance)\n *\n * @param options - Configuration options\n * @returns Promise resolving to configured DatabaseInterface\n *\n * @example\n * ```typescript\n * // Basic usage - all registered schemas\n * const db = await getTestDatabase();\n *\n * // Specific classes only\n * const db = await getTestDatabase({ classes: ['Council', 'Meeting'] });\n *\n * // JSON adapter instead of SQLite\n * const db = await getTestDatabase({ type: 'json' });\n *\n * // File-based database for persistent tests\n * const db = await getTestDatabase({ type: 'sqlite', url: '/tmp/test.db' });\n *\n * // Initialize schemas in an existing database\n * const myDb = await getDatabase({ type: 'sqlite', url: ':memory:' });\n * await getTestDatabase({ db: myDb });\n *\n * // Skip system tables (rare use case)\n * const db = await getTestDatabase({ includeSystemTables: false });\n * ```\n */\nexport async function getTestDatabase(\n options: TestDatabaseOptions = {},\n): Promise<DatabaseInterface> {\n const {\n type = 'sqlite',\n url = ':memory:',\n db: existingDb,\n classes,\n includeSystemTables = true,\n } = options;\n\n // Use existing database or create new one\n const db =\n existingDb ??\n (await getDatabase({\n type,\n url,\n __smrtSkipVitestSchemaPreparation: true,\n } as TestDatabaseConnectionOptions));\n\n // Initialize system tables (same as production)\n if (includeSystemTables) {\n await initializeSystemTables(db);\n }\n\n // Get class names to setup\n const classNames = resolveRequestedSchemaClassNames(\n classes ?? ObjectRegistry.getQualifiedClassNames(),\n );\n\n // Skip if no classes registered\n if (classNames.length === 0) {\n return db;\n }\n\n // Use the same schema generation as production\n const schemaGenerator = new SchemaGenerator();\n const ddlEngine =\n type === 'json' || typeof (db as any).exportTable === 'function'\n ? 'json'\n : 'sqlite';\n\n // Track created tables to avoid duplicates (STI base classes)\n const createdTables = new Set<string>();\n\n for (const className of classNames) {\n // R11: the registration carries idType (native uuid vs text); read it\n // below when building runtimeSchemaConfig.\n const registered = ObjectRegistry.getClass(className);\n // Skip STI children - their schema is part of the base class table.\n // main (#1324): isSTIChild() compares RegisteredClass *identity* rather\n // than raw strings, so it stays correct under R5-canon (getSTIBase returns\n // qualified names) while also handling collection/override registrations.\n if (isSTIChild(className)) {\n continue;\n }\n\n const tableName = ObjectRegistry.getTableName(className);\n if (!tableName || createdTables.has(tableName)) {\n continue;\n }\n\n const fields = await ObjectRegistry.getAllFields(className);\n const strategy = ObjectRegistry.getTableStrategy(className);\n const runtimeSchemaConfig = {\n conflictColumns: ObjectRegistry.getConflictColumns(className),\n idType: registered?.config.idType,\n registry: ObjectRegistry,\n };\n\n // Generate schema using SchemaGenerator (same as migrations)\n const schema =\n strategy === 'sti'\n ? await schemaGenerator.generateSTISchemaFromRegistry(\n className,\n tableName,\n fields,\n runtimeSchemaConfig,\n )\n : schemaGenerator.generateSchemaFromRegistry(\n className,\n tableName,\n fields,\n runtimeSchemaConfig,\n );\n\n // Generate DDL using generateSQL() - the single source of truth\n const ddl = schemaGenerator.generateSQL(schema, ddlEngine);\n\n try {\n await db.query(ddl);\n createdTables.add(tableName);\n\n // Create indexes (use DDL strategy so jsonPath / where / etc. render)\n const ddlStrategy = (\n await import('../schema/ddl/index.js')\n ).getDDLStrategy(ddlEngine);\n const indexStatements = ddlStrategy.generateIndexes(schema);\n for (const indexSQL of indexStatements) {\n try {\n await db.query(indexSQL);\n } catch (indexError) {\n // Log but don't fail on index creation errors\n // Some indexes may fail if columns don't exist (STI meta fields)\n console.warn(\n `[getTestDatabase] Warning: Failed to create index: ${indexError instanceof Error ? indexError.message : String(indexError)} (SQL: ${indexSQL})`,\n );\n }\n }\n } catch (error) {\n // Provide helpful error message for table creation failures\n throw new Error(\n `[getTestDatabase] Failed to create table '${tableName}' for class '${className}': ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n }\n\n return db;\n}\n\n/**\n * Initialize SMRT system tables in a database\n *\n * System tables use _smrt_ prefix and store framework metadata.\n * All statements use `IF NOT EXISTS` for idempotency.\n *\n * @param db - Database interface to initialize\n */\nasync function initializeSystemTables(db: DatabaseInterface): Promise<void> {\n await ensureLegacySystemTableCompatibility(db);\n\n // Split multi-statement SQL into individual statements\n const allStatements: string[] = [];\n for (const multiStatementSQL of ALL_SYSTEM_TABLES) {\n const statements = multiStatementSQL\n .split(';')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n allStatements.push(...statements);\n }\n\n // Use db.query() — system tables use CREATE TABLE/INDEX IF NOT EXISTS\n // which databases handle natively without per-column existence checks.\n for (const statement of allStatements) {\n await db.query(statement);\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"database.js","sources":["../../src/testing/database.ts"],"sourcesContent":["/**\n * Test database utilities for SMRT framework\n *\n * Provides `getTestDatabase()` which creates an in-memory database with all\n * registered SMRT object schemas. Uses the same schema generation logic as\n * the migration system to ensure consistency between test and production.\n *\n * @example\n * ```typescript\n * import { getTestDatabase } from '@happyvertical/smrt-core/testing';\n *\n * beforeEach(async () => {\n * const db = await getTestDatabase();\n * collection = await MyCollection.create({ db });\n * });\n * ```\n */\n\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { getDatabase } from '@happyvertical/sql';\nimport {\n type CollectionRegistrationLookup,\n isCollectionRegistration,\n resolveCollectionItemClassName,\n resolveRelatedRegistration,\n} from '../registry/collection-resolution.js';\nimport { ObjectRegistry } from '../registry.js';\nimport { SchemaGenerator } from '../schema/generator.js';\nimport { ensureLegacySystemTableCompatibility } from '../system/compatibility.js';\nimport { ALL_SYSTEM_TABLES } from '../system/schema.js';\n\ntype TestDatabaseConnectionOptions = Parameters<typeof getDatabase>[0] & {\n __smrtSkipVitestSchemaPreparation?: boolean;\n};\n\nfunction resolveSTIBaseRegistration(className: string, stiBaseName: string) {\n const registered = ObjectRegistry.getClass(className);\n\n if (stiBaseName.includes(':')) {\n return ObjectRegistry.getClass(stiBaseName);\n }\n\n if (registered?.packageName) {\n const samePackageBase = ObjectRegistry.getClassInPackage(\n registered.packageName,\n stiBaseName,\n );\n if (samePackageBase) {\n return samePackageBase;\n }\n }\n\n return ObjectRegistry.getClass(stiBaseName);\n}\n\nfunction resolveSTIBaseLookupName(\n className: string,\n stiBaseName: string,\n): string {\n const stiBase = resolveSTIBaseRegistration(className, stiBaseName);\n return stiBase?.qualifiedName || stiBase?.name || stiBaseName;\n}\n\nfunction isSTIChild(className: string): boolean {\n const stiBaseName = ObjectRegistry.getSTIBase(className);\n if (!stiBaseName) {\n return false;\n }\n\n const registered = ObjectRegistry.getClass(className);\n const stiBase = resolveSTIBaseRegistration(className, stiBaseName);\n\n if (registered && stiBase) {\n return registered !== stiBase;\n }\n\n return stiBaseName !== className;\n}\n\ntype RegisteredSchemaClass = NonNullable<\n ReturnType<typeof ObjectRegistry.getClass>\n>;\n\nconst collectionRegistrationLookup: CollectionRegistrationLookup = {\n findClass: (className) => ObjectRegistry.getClass(className),\n findClassInPackage: (packageName, className) =>\n ObjectRegistry.getClassInPackage(packageName, className),\n getInheritanceChain: (className) =>\n ObjectRegistry.getInheritanceChain(className),\n};\n\nfunction resolveCollectionSchemaClassName(\n className: string,\n registered: RegisteredSchemaClass,\n): string {\n const itemClassName = resolveCollectionItemClassName(\n className,\n registered,\n collectionRegistrationLookup,\n );\n if (itemClassName) {\n const itemRegistration = resolveRelatedRegistration(\n itemClassName,\n registered,\n collectionRegistrationLookup,\n );\n const itemLookupName =\n itemRegistration?.qualifiedName ||\n itemRegistration?.name ||\n itemClassName;\n const stiBase = ObjectRegistry.getSTIBase(itemLookupName);\n return stiBase\n ? resolveSTIBaseLookupName(itemLookupName, stiBase)\n : itemLookupName;\n }\n\n const tableName =\n registered.schema?.tableName ||\n registered.config.tableName ||\n ObjectRegistry.getTableName(className);\n if (!tableName) {\n return className;\n }\n\n const collectionPackage = registered.packageName;\n\n for (const candidate of ObjectRegistry.getAllClasses().values()) {\n if (\n candidate === registered ||\n isCollectionRegistration(\n candidate.qualifiedName || candidate.name,\n candidate,\n collectionRegistrationLookup,\n )\n ) {\n continue;\n }\n\n const candidateTableName =\n candidate.schema?.tableName || candidate.config.tableName;\n if (candidateTableName !== tableName) {\n continue;\n }\n\n if (\n collectionPackage &&\n candidate.packageName &&\n candidate.packageName !== collectionPackage\n ) {\n continue;\n }\n\n const candidateLookupName = candidate.qualifiedName || candidate.name;\n const stiBase = ObjectRegistry.getSTIBase(candidateLookupName);\n return stiBase\n ? resolveSTIBaseLookupName(candidateLookupName, stiBase)\n : candidateLookupName;\n }\n\n return className;\n}\n\n/**\n * Options for creating a test database\n */\nexport interface TestDatabaseOptions {\n /**\n * Database type (default: 'sqlite')\n * - 'sqlite': SQLite database\n * - 'json': JSON adapter (stores data as JSON files with DuckDB for querying)\n */\n type?: 'sqlite' | 'json';\n\n /**\n * Database URL (default: ':memory:')\n * Use ':memory:' for in-memory databases (fastest for tests)\n * Or provide a file path for persistent test databases\n */\n url?: string;\n\n /**\n * Pre-existing database to initialize schemas in.\n * If provided, `type` and `url` are ignored.\n */\n db?: DatabaseInterface;\n\n /**\n * Specific classes to setup schemas for.\n * If not provided, sets up schemas for all registered classes.\n */\n classes?: string[];\n\n /**\n * Whether to include system tables (default: true)\n * System tables include _smrt_contexts, _smrt_migrations, etc.\n */\n includeSystemTables?: boolean;\n}\n\nfunction resolveRequestedSchemaClassName(className: string): string {\n const registered = ObjectRegistry.getClass(className);\n if (!registered) {\n return className;\n }\n\n if (\n !isCollectionRegistration(\n className,\n registered,\n collectionRegistrationLookup,\n )\n ) {\n const stiBase = ObjectRegistry.getSTIBase(className);\n return stiBase ? resolveSTIBaseLookupName(className, stiBase) : className;\n }\n\n return resolveCollectionSchemaClassName(className, registered);\n}\n\nfunction resolveRequestedSchemaClassNames(classNames: string[]): string[] {\n const resolved: string[] = [];\n const seen = new Set<string>();\n\n for (const className of classNames) {\n const schemaClassName = resolveRequestedSchemaClassName(className);\n if (seen.has(schemaClassName)) {\n continue;\n }\n\n seen.add(schemaClassName);\n resolved.push(schemaClassName);\n }\n\n return resolved;\n}\n\n/**\n * Creates an in-memory test database with schemas pre-created\n *\n * This utility is designed for testing. It creates an in-memory database\n * and initializes all registered SMRT object schemas using the **same\n * schema generation logic** as the production migration system.\n *\n * **Key features:**\n * - Uses `SchemaGenerator.generateSQL()` - the single source of truth for DDL\n * - Handles STI (Single Table Inheritance) correctly\n * - Creates system tables for framework functionality\n * - Safe for parallel test execution (each call creates isolated instance)\n *\n * @param options - Configuration options\n * @returns Promise resolving to configured DatabaseInterface\n *\n * @example\n * ```typescript\n * // Basic usage - all registered schemas\n * const db = await getTestDatabase();\n *\n * // Specific classes only\n * const db = await getTestDatabase({ classes: ['Council', 'Meeting'] });\n *\n * // JSON adapter instead of SQLite\n * const db = await getTestDatabase({ type: 'json' });\n *\n * // File-based database for persistent tests\n * const db = await getTestDatabase({ type: 'sqlite', url: '/tmp/test.db' });\n *\n * // Initialize schemas in an existing database\n * const myDb = await getDatabase({ type: 'sqlite', url: ':memory:' });\n * await getTestDatabase({ db: myDb });\n *\n * // Skip system tables (rare use case)\n * const db = await getTestDatabase({ includeSystemTables: false });\n * ```\n */\nexport async function getTestDatabase(\n options: TestDatabaseOptions = {},\n): Promise<DatabaseInterface> {\n const {\n type = 'sqlite',\n url = ':memory:',\n db: existingDb,\n classes,\n includeSystemTables = true,\n } = options;\n\n // Use existing database or create new one\n const db =\n existingDb ??\n (await getDatabase({\n type,\n url,\n __smrtSkipVitestSchemaPreparation: true,\n } as TestDatabaseConnectionOptions));\n\n // Initialize system tables (same as production)\n if (includeSystemTables) {\n await initializeSystemTables(db);\n }\n\n // Get class names to setup\n const classNames = resolveRequestedSchemaClassNames(\n classes ?? ObjectRegistry.getQualifiedClassNames(),\n );\n\n // Skip if no classes registered\n if (classNames.length === 0) {\n return db;\n }\n\n // Use the same schema generation as production\n const schemaGenerator = new SchemaGenerator();\n const ddlEngine =\n type === 'json' ||\n typeof (db as { exportTable?: unknown }).exportTable === 'function'\n ? 'json'\n : 'sqlite';\n\n // Track created tables to avoid duplicates (STI base classes)\n const createdTables = new Set<string>();\n\n for (const className of classNames) {\n // R11: the registration carries idType (native uuid vs text); read it\n // below when building runtimeSchemaConfig.\n const registered = ObjectRegistry.getClass(className);\n // Skip STI children - their schema is part of the base class table.\n // main (#1324): isSTIChild() compares RegisteredClass *identity* rather\n // than raw strings, so it stays correct under R5-canon (getSTIBase returns\n // qualified names) while also handling collection/override registrations.\n if (isSTIChild(className)) {\n continue;\n }\n\n const tableName = ObjectRegistry.getTableName(className);\n if (!tableName || createdTables.has(tableName)) {\n continue;\n }\n\n const fields = await ObjectRegistry.getAllFields(className);\n const strategy = ObjectRegistry.getTableStrategy(className);\n const runtimeSchemaConfig = {\n conflictColumns: ObjectRegistry.getConflictColumns(className),\n idType: registered?.config.idType,\n registry: ObjectRegistry,\n };\n\n // Generate schema using SchemaGenerator (same as migrations)\n const schema =\n strategy === 'sti'\n ? await schemaGenerator.generateSTISchemaFromRegistry(\n className,\n tableName,\n fields,\n runtimeSchemaConfig,\n )\n : schemaGenerator.generateSchemaFromRegistry(\n className,\n tableName,\n fields,\n runtimeSchemaConfig,\n );\n\n // Generate DDL using generateSQL() - the single source of truth\n const ddl = schemaGenerator.generateSQL(schema, ddlEngine);\n\n try {\n await db.query(ddl);\n createdTables.add(tableName);\n\n // Create indexes (use DDL strategy so jsonPath / where / etc. render)\n const ddlStrategy = (\n await import('../schema/ddl/index.js')\n ).getDDLStrategy(ddlEngine);\n const indexStatements = ddlStrategy.generateIndexes(schema);\n for (const indexSQL of indexStatements) {\n try {\n await db.query(indexSQL);\n } catch (indexError) {\n // Log but don't fail on index creation errors\n // Some indexes may fail if columns don't exist (STI meta fields)\n console.warn(\n `[getTestDatabase] Warning: Failed to create index: ${indexError instanceof Error ? indexError.message : String(indexError)} (SQL: ${indexSQL})`,\n );\n }\n }\n } catch (error) {\n // Provide helpful error message for table creation failures\n throw new Error(\n `[getTestDatabase] Failed to create table '${tableName}' for class '${className}': ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n }\n\n return db;\n}\n\n/**\n * Initialize SMRT system tables in a database\n *\n * System tables use _smrt_ prefix and store framework metadata.\n * All statements use `IF NOT EXISTS` for idempotency.\n *\n * @param db - Database interface to initialize\n */\nasync function initializeSystemTables(db: DatabaseInterface): Promise<void> {\n await ensureLegacySystemTableCompatibility(db);\n\n // Split multi-statement SQL into individual statements\n const allStatements: string[] = [];\n for (const multiStatementSQL of ALL_SYSTEM_TABLES) {\n const statements = multiStatementSQL\n .split(';')\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n allStatements.push(...statements);\n }\n\n // Use db.query() — system tables use CREATE TABLE/INDEX IF NOT EXISTS\n // which databases handle natively without per-column existence checks.\n for (const statement of allStatements) {\n await db.query(statement);\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/utils/json.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json.js","sources":["../../src/utils/json.ts"],"sourcesContent":["/**\n * JSON utilities with optional SIMD acceleration\n *\n * Uses @happyvertical/json for faster parsing when available,\n * with automatic fallback to native JSON.\n *\n * Performance: 2-3x faster parsing on large JSON files (680KB manifests)\n */\n\nimport {\n type JSONAdapter,\n JSONFactory,\n type ParseError,\n type Result,\n type StringifyError,\n} from '@happyvertical/json';\n\n// Singleton adapter instance\nlet adapter: JSONAdapter | null = null;\n\n/**\n * Get or create the JSON adapter instance\n */\nfunction getAdapter(): JSONAdapter {\n if (!adapter) {\n adapter = JSONFactory.create({\n adapter: 'auto', // Use SIMD when available, fallback to native\n fallback: true,\n });\n }\n return adapter;\n}\n\n/**\n * Parse JSON string with optional SIMD acceleration\n *\n * @example\n * ```typescript\n * import { parse } from '@happyvertical/smrt-core/utils/json';\n *\n * const data = parse<MyType>('{\"key\": \"value\"}');\n * ```\n */\nexport function parse<T = unknown>(text: string): T {\n return getAdapter().parse<T>(text);\n}\n\n/**\n * Stringify value to JSON with optional SIMD acceleration\n *\n * @example\n * ```typescript\n * import { stringify } from '@happyvertical/smrt-core/utils/json';\n *\n * const json = stringify({ key: 'value' });\n * const pretty = stringify({ key: 'value' }, null, 2);\n * ```\n */\nexport function stringify(\n value: unknown,\n replacer?:\n | ((key: string, value: unknown) => unknown)\n | (string | number)[]\n | null,\n space?: number | string,\n): string {\n return getAdapter().stringify(value, replacer
|
|
1
|
+
{"version":3,"file":"json.js","sources":["../../src/utils/json.ts"],"sourcesContent":["/**\n * JSON utilities with optional SIMD acceleration\n *\n * Uses @happyvertical/json for faster parsing when available,\n * with automatic fallback to native JSON.\n *\n * Performance: 2-3x faster parsing on large JSON files (680KB manifests)\n */\n\nimport {\n type JSONAdapter,\n JSONFactory,\n type ParseError,\n type Result,\n type StringifyError,\n} from '@happyvertical/json';\n\n// Singleton adapter instance\nlet adapter: JSONAdapter | null = null;\n\n/**\n * Get or create the JSON adapter instance\n */\nfunction getAdapter(): JSONAdapter {\n if (!adapter) {\n adapter = JSONFactory.create({\n adapter: 'auto', // Use SIMD when available, fallback to native\n fallback: true,\n });\n }\n return adapter;\n}\n\n/**\n * Parse JSON string with optional SIMD acceleration\n *\n * @example\n * ```typescript\n * import { parse } from '@happyvertical/smrt-core/utils/json';\n *\n * const data = parse<MyType>('{\"key\": \"value\"}');\n * ```\n */\nexport function parse<T = unknown>(text: string): T {\n return getAdapter().parse<T>(text);\n}\n\n/**\n * Stringify value to JSON with optional SIMD acceleration\n *\n * @example\n * ```typescript\n * import { stringify } from '@happyvertical/smrt-core/utils/json';\n *\n * const json = stringify({ key: 'value' });\n * const pretty = stringify({ key: 'value' }, null, 2);\n * ```\n */\nexport function stringify(\n value: unknown,\n replacer?:\n | ((key: string, value: unknown) => unknown)\n | (string | number)[]\n | null,\n space?: number | string,\n): string {\n return getAdapter().stringify(value, replacer, space);\n}\n\n/**\n * Deep clone via JSON round-trip with optional SIMD acceleration\n *\n * @example\n * ```typescript\n * import { clone } from '@happyvertical/smrt-core/utils/json';\n *\n * const copy = clone(original);\n * ```\n */\nexport function clone<T>(value: T): T {\n return getAdapter().clone(value);\n}\n\n/**\n * Parse JSON without throwing - returns Result type\n *\n * @example\n * ```typescript\n * import { safeParse } from '@happyvertical/smrt-core/utils/json';\n *\n * const result = safeParse<Config>(configJson);\n * if (result.success) {\n * console.log(result.value);\n * } else {\n * console.error(result.error.message);\n * }\n * ```\n */\nexport function safeParse<T = unknown>(text: string): Result<T, ParseError> {\n return getAdapter().safeParse<T>(text);\n}\n\n/**\n * Stringify without throwing - returns Result type\n *\n * @example\n * ```typescript\n * import { safeStringify } from '@happyvertical/smrt-core/utils/json';\n *\n * const result = safeStringify(data);\n * if (result.success) {\n * console.log(result.value);\n * } else {\n * console.error(result.error.message);\n * }\n * ```\n */\nexport function safeStringify(value: unknown): Result<string, StringifyError> {\n return getAdapter().safeStringify(value);\n}\n\n/**\n * Check if a string is valid JSON\n *\n * @example\n * ```typescript\n * import { isValid } from '@happyvertical/smrt-core/utils/json';\n *\n * if (isValid(userInput)) {\n * // Safe to parse\n * }\n * ```\n */\nexport function isValid(text: string): boolean {\n return getAdapter().isValid(text);\n}\n\n/**\n * Get information about the active JSON adapter\n *\n * @example\n * ```typescript\n * import { getAdapterInfo } from '@happyvertical/smrt-core/utils/json';\n *\n * const info = getAdapterInfo();\n * console.log(`Using ${info.name} adapter, SIMD: ${info.simdEnabled}`);\n * ```\n */\nexport function getAdapterInfo(): {\n name: string;\n isNative: boolean;\n version?: string;\n simdEnabled?: boolean;\n} {\n return getAdapter().getInfo();\n}\n\n/**\n * Re-export types for convenience\n */\nexport type { JSONAdapter, ParseError, Result, StringifyError };\n"],"names":[],"mappings":";AAkBA,IAAI,UAA8B;AAKlC,SAAS,aAA0B;AACjC,MAAI,CAAC,SAAS;AACZ,cAAU,YAAY,OAAO;AAAA,MAC3B,SAAS;AAAA;AAAA,MACT,UAAU;AAAA,IAAA,CACX;AAAA,EACH;AACA,SAAO;AACT;AAYO,SAAS,MAAmB,MAAiB;AAClD,SAAO,WAAA,EAAa,MAAS,IAAI;AACnC;AAaO,SAAS,UACd,OACA,UAIA,OACQ;AACR,SAAO,WAAA,EAAa,UAAU,OAAO,UAAU,KAAK;AACtD;AAYO,SAAS,MAAS,OAAa;AACpC,SAAO,WAAA,EAAa,MAAM,KAAK;AACjC;AAiBO,SAAS,UAAuB,MAAqC;AAC1E,SAAO,WAAA,EAAa,UAAa,IAAI;AACvC;AAiBO,SAAS,cAAc,OAAgD;AAC5E,SAAO,WAAA,EAAa,cAAc,KAAK;AACzC;AAcO,SAAS,QAAQ,MAAuB;AAC7C,SAAO,WAAA,EAAa,QAAQ,IAAI;AAClC;AAaO,SAAS,iBAKd;AACA,SAAO,WAAA,EAAa,QAAA;AACtB;"}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SmrtObject } from './object';
|
|
1
2
|
import { classnameToTablename, pluralize, toSnakeCase } from './utils/naming.js';
|
|
2
3
|
export { classnameToTablename, pluralize, toSnakeCase };
|
|
3
4
|
/**
|
|
@@ -19,14 +20,14 @@ export declare function toCamelCase(str: string): string;
|
|
|
19
20
|
* @param obj - Object with camelCase keys
|
|
20
21
|
* @returns Object with snake_case keys
|
|
21
22
|
*/
|
|
22
|
-
export declare function keysToSnakeCase(obj: Record<string,
|
|
23
|
+
export declare function keysToSnakeCase(obj: Record<string, unknown>): Record<string, unknown>;
|
|
23
24
|
/**
|
|
24
25
|
* Converts all keys in an object from snake_case to camelCase
|
|
25
26
|
*
|
|
26
27
|
* @param obj - Object with snake_case keys
|
|
27
28
|
* @returns Object with camelCase keys
|
|
28
29
|
*/
|
|
29
|
-
export declare function keysToCamelCase(obj: Record<string,
|
|
30
|
+
export declare function keysToCamelCase(obj: Record<string, unknown>): Record<string, unknown>;
|
|
30
31
|
/**
|
|
31
32
|
* Checks if a field name indicates a date field based on naming conventions
|
|
32
33
|
*
|
|
@@ -81,7 +82,7 @@ export declare function dateAsObject(date: Date | string): string;
|
|
|
81
82
|
* console.log(fields.price.type); // 'REAL'
|
|
82
83
|
* ```
|
|
83
84
|
*/
|
|
84
|
-
export declare function fieldsFromClass(ClassType: new (...args:
|
|
85
|
+
export declare function fieldsFromClass(ClassType: new (...args: never[]) => SmrtObject, values?: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
85
86
|
/**
|
|
86
87
|
* Returns the old (incorrect) pluralized table name for migration purposes.
|
|
87
88
|
*
|
|
@@ -146,18 +147,25 @@ export declare function generateTableRenameMigrations(classNames: string[]): str
|
|
|
146
147
|
* tableNameFromClass(Category); // "categories" (derived from runtime name)
|
|
147
148
|
* ```
|
|
148
149
|
*/
|
|
149
|
-
export declare function tableNameFromClass(ClassType: Function | (new (...args:
|
|
150
|
+
export declare function tableNameFromClass(ClassType: Function | (new (...args: never[]) => SmrtObject)): string;
|
|
150
151
|
/**
|
|
151
152
|
* Formats data for JavaScript by converting date strings to Date objects
|
|
152
153
|
* and snake_case column names to camelCase properties
|
|
153
154
|
*
|
|
155
|
+
* Generic over the input row type `T` so callers preserve their (typically
|
|
156
|
+
* loose) row typing across the hydration boundary — this matches the historic
|
|
157
|
+
* behavior where a `Record<string, any>` / `any` row produced an equally loose
|
|
158
|
+
* result. Keys are re-cased and values are type-converted at runtime; the
|
|
159
|
+
* return is the same `Record` shape, so it is reasserted as `T` at this DB
|
|
160
|
+
* boundary rather than widened to an unrelated type.
|
|
161
|
+
*
|
|
154
162
|
* @param data - Object with data to format (snake_case column names from DB)
|
|
155
163
|
* @param fields - Optional field definitions to determine types (from fieldsFromClass)
|
|
156
164
|
* @returns Object with properly typed values and camelCase property names for JavaScript
|
|
157
165
|
*/
|
|
158
|
-
export declare function formatDataJs
|
|
166
|
+
export declare function formatDataJs<T extends Record<string, unknown> = Record<string, unknown>>(data: T, fields?: Record<string, {
|
|
159
167
|
type?: string;
|
|
160
|
-
}>):
|
|
168
|
+
}>): T;
|
|
161
169
|
/**
|
|
162
170
|
* Type guard to check if a value is a Field instance
|
|
163
171
|
*
|
|
@@ -165,7 +173,7 @@ export declare function formatDataJs(data: Record<string, any>, fields?: Record<
|
|
|
165
173
|
* @param value - Value to check
|
|
166
174
|
* @returns Always false (Field class no longer exists)
|
|
167
175
|
*/
|
|
168
|
-
export declare function isFieldInstance(value:
|
|
176
|
+
export declare function isFieldInstance(value: unknown): value is never;
|
|
169
177
|
/**
|
|
170
178
|
* Formats data for SQL by converting Date objects to ISO strings
|
|
171
179
|
* and camelCase property names to snake_case column names
|
|
@@ -173,5 +181,5 @@ export declare function isFieldInstance(value: any): value is never;
|
|
|
173
181
|
* @param data - Object with data to format (camelCase property names from JavaScript)
|
|
174
182
|
* @returns Object with properly formatted values and snake_case column names for SQL
|
|
175
183
|
*/
|
|
176
|
-
export declare function formatDataSql(data: Record<string,
|
|
184
|
+
export declare function formatDataSql(data: Record<string, unknown>): Record<string, unknown>;
|
|
177
185
|
//# sourceMappingURL=utils.d.ts.map
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAG3C,OAAO,EACL,oBAAoB,EACpB,SAAS,EACT,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAQ3B,OAAO,EAAE,oBAAoB,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC;AAExD;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMzB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMzB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,WAItC;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,QAK/C;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,UAK/C;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,UAAU,EAC/C,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,oCAmCjC;AAMD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMpE;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAa5E;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAEhC,SAAS,EAAE,QAAQ,GAAG,CAAC,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,UAAU,CAAC,UAiB7D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAC1B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,CAAC,CAuHxD;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,KAAK,CAE9D;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,2BAc1D"}
|
package/dist/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sources":["../src/utils.ts"],"sourcesContent":["import { createLogger } from '@happyvertical/logger';\nimport { ObjectRegistry } from './registry';\nimport {\n classnameToTablename,\n pluralize,\n toSnakeCase,\n} from './utils/naming.js';\n\n// formatDataJs' debug traces are gated by DEBUG_STI, so the level must allow\n// debug when it's set (a fixed 'info' would filter them out and break the flag).\nconst logger = createLogger({\n level: process.env.DEBUG_STI ? 'debug' : 'info',\n});\n\nexport { classnameToTablename, pluralize, toSnakeCase };\n\n/**\n * Converts a snake_case string to camelCase\n *\n * @param str - String in snake_case format\n * @returns String in camelCase format\n * @example\n * ```typescript\n * toCamelCase('meetings_url'); // 'meetingsUrl'\n * toCamelCase('created_at'); // 'createdAt'\n * toCamelCase('id'); // 'id'\n * ```\n */\nexport function toCamelCase(str: string): string {\n return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());\n}\n\n/**\n * Converts all keys in an object from camelCase to snake_case\n *\n * @param obj - Object with camelCase keys\n * @returns Object with snake_case keys\n */\nexport function keysToSnakeCase(obj: Record<string, any>): Record<string, any> {\n const result: Record<string, any> = {};\n for (const [key, value] of Object.entries(obj)) {\n result[toSnakeCase(key)] = value;\n }\n return result;\n}\n\n/**\n * Converts all keys in an object from snake_case to camelCase\n *\n * @param obj - Object with snake_case keys\n * @returns Object with camelCase keys\n */\nexport function keysToCamelCase(obj: Record<string, any>): Record<string, any> {\n const result: Record<string, any> = {};\n for (const [key, value] of Object.entries(obj)) {\n result[toCamelCase(key)] = value;\n }\n return result;\n}\n\n/**\n * Checks if a field name indicates a date field based on naming conventions\n *\n * Recognizes common date field patterns like '_at', '_date', and 'date'.\n * Used for automatic type inference during schema generation.\n *\n * @param key - Field name to check\n * @returns Boolean indicating if the field is likely a date field\n * @example\n * ```typescript\n * isDateField('created_at'); // true\n * isDateField('updated_date'); // true\n * isDateField('name'); // false\n * ```\n */\nexport function isDateField(key: string) {\n // Fallback pattern matching when schema is not available\n // Primary date detection should use schema field types\n return key.endsWith('_date') || key.endsWith('_at') || key === 'date';\n}\n\n/**\n * Converts a date string to a Date object\n *\n * @param date - Date as string or Date object\n * @returns Date object\n */\nexport function dateAsString(date: Date | string) {\n if (typeof date === 'string') {\n return new Date(date);\n }\n return date;\n}\n\n/**\n * Converts a Date object to an ISO string\n *\n * @param date - Date as Date object or string\n * @returns ISO date string or the original string\n */\nexport function dateAsObject(date: Date | string) {\n if (date instanceof Date) {\n return date.toISOString();\n }\n return date;\n}\n\n/**\n * Extracts field definitions from a class constructor\n *\n * Uses ObjectRegistry cached fields from AST manifest exclusively.\n * No runtime introspection fallback - classes must be decorated with @smrt()\n * for schema generation to work.\n *\n * @param ClassType - Class constructor to extract fields from\n * @param values - Optional values to set for the fields\n * @returns Object containing field definitions with names, types, and values\n * @throws {Error} If the class is not registered in ObjectRegistry\n * @example\n * ```typescript\n * @smrt()\n * class Product extends SmrtObject {\n * name: string = '';\n * price: number = 0.0;\n * }\n *\n * const fields = await fieldsFromClass(Product);\n * console.log(fields.name.type); // 'TEXT'\n * console.log(fields.price.type); // 'REAL'\n * ```\n */\nexport async function fieldsFromClass(\n ClassType: new (...args: any[]) => any,\n values?: Record<string, any>,\n) {\n const className =\n ObjectRegistry.getClassByConstructor(ClassType as any)?.name ||\n ClassType.name;\n // NEW: Use getAllFields() to include inherited fields from parent classes\n const cachedFields = await ObjectRegistry.getAllFields(className);\n\n // Phase 2: AST manifest only - no runtime introspection fallback\n if (cachedFields.size === 0) {\n // Return empty fields for unregistered classes (for backward compatibility)\n // generateSchema() will throw if it needs field definitions\n return {};\n }\n\n // Use cached field definitions from AST manifest\n const fields: Record<string, any> = {};\n\n // Add/override with fields from cached registry\n for (const [key, field] of cachedFields.entries()) {\n const meta = { ...(field._meta || {}) };\n delete meta.__smrtSystemField;\n\n fields[key] = {\n name: key,\n type: field.type || 'TEXT',\n _meta: meta,\n ...(values && key in values ? { value: values[key] } : {}),\n };\n }\n\n return fields;\n}\n\n// NOTE: generateSchema moved to schema/utils.ts\n// to prevent bundling Node.js-only code (SchemaGenerator with node:crypto) in browser builds.\n// Import from './schema/utils' in Node.js code that needs schema generation.\n\n/**\n * Returns the old (incorrect) pluralized table name for migration purposes.\n *\n * This function replicates the previous buggy behavior where 'y' → 'ies'\n * transformation was applied AFTER adding 's', resulting in incorrect\n * pluralization (e.g., 'currency' → 'currencys' instead of 'currencies').\n *\n * Use this to generate migration SQL that renames old tables to new names.\n *\n * @param className - Name of the class\n * @returns The incorrectly pluralized table name (old behavior)\n * @example\n * ```typescript\n * // Generate migration for a class\n * const oldName = legacyClassnameToTablename('Currency'); // 'currencys'\n * const newName = classnameToTablename('Currency'); // 'currencies'\n * console.log(`ALTER TABLE ${oldName} RENAME TO ${newName};`);\n * ```\n */\nexport function legacyClassnameToTablename(className: string): string {\n return className\n .replace(/([a-z])([A-Z])/g, '$1_$2')\n .toLowerCase()\n .replace(/([^s])$/, '$1s')\n .replace(/y$/, 'ies'); // This runs AFTER 's' is added, so it's a no-op for 'y' words\n}\n\n/**\n * Generates migration SQL for renaming tables from old to new naming convention.\n *\n * Returns an array of SQL statements to rename tables that were affected\n * by the pluralization bug (Issue #839).\n *\n * @param classNames - Array of class names to check for migration\n * @returns Array of SQL RENAME statements (empty if no changes needed)\n * @example\n * ```typescript\n * const migrations = generateTableRenameMigrations([\n * 'Currency',\n * 'JournalEntry',\n * 'Product' // Not affected\n * ]);\n * // Returns:\n * // [\n * // 'ALTER TABLE currencys RENAME TO currencies;',\n * // 'ALTER TABLE journal_entrys RENAME TO journal_entries;'\n * // ]\n * ```\n */\nexport function generateTableRenameMigrations(classNames: string[]): string[] {\n const migrations: string[] = [];\n\n for (const className of classNames) {\n const oldName = legacyClassnameToTablename(className);\n const newName = classnameToTablename(className);\n\n if (oldName !== newName) {\n migrations.push(`ALTER TABLE ${oldName} RENAME TO ${newName};`);\n }\n }\n\n return migrations;\n}\n\n/**\n * Generates a table name from a class constructor\n *\n * Checks for SMRT_TABLE_NAME static property first (set by @smrt() decorator),\n * which survives code minification. Falls back to deriving from ClassType.name\n * for backward compatibility.\n *\n * @param ClassType - Class constructor or function\n * @returns Pluralized snake_case table name\n * @example\n * ```typescript\n * // With @smrt() decorator (recommended)\n * @smrt()\n * class Product extends SmrtObject { }\n * tableNameFromClass(Product); // \"products\" (captured before minification)\n *\n * // Without decorator (fallback)\n * class Category extends SmrtObject { }\n * tableNameFromClass(Category); // \"categories\" (derived from runtime name)\n * ```\n */\nexport function tableNameFromClass(\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n ClassType: Function | (new (...args: any[]) => any),\n) {\n // Check for SMRT_TABLE_NAME property set by @smrt() decorator (survives minification)\n if ('SMRT_TABLE_NAME' in ClassType) {\n return (ClassType as any).SMRT_TABLE_NAME;\n }\n\n // Fallback: derive from class name (breaks with minification)\n const snakeCase = ClassType.name\n // Insert underscore between lower & upper case letters\n .replace(/([a-z])([A-Z])/g, '$1_$2')\n // Convert to lowercase\n .toLowerCase();\n\n return pluralize(snakeCase);\n}\n\n/**\n * Formats data for JavaScript by converting date strings to Date objects\n * and snake_case column names to camelCase properties\n *\n * @param data - Object with data to format (snake_case column names from DB)\n * @param fields - Optional field definitions to determine types (from fieldsFromClass)\n * @returns Object with properly typed values and camelCase property names for JavaScript\n */\nexport function formatDataJs(\n data: Record<string, any>,\n fields?: Record<string, { type?: string }>,\n) {\n const normalizedData: Record<string, any> = {};\n\n if (process.env.DEBUG_STI) {\n logger.debug('[formatDataJs] Input data', {\n hasMetaData: !!data._meta_data,\n metaType: data._meta_type,\n keys: Object.keys(data),\n });\n }\n\n // STI: If _meta_data exists, merge it into a FRESH object first so meta\n // fields are available during formatting. This must never mutate the caller's\n // `data` argument — `formatDataJs` is a public export and external callers may\n // pass shared objects that would otherwise get silent field injection (#1378).\n let mergedData: Record<string, any> = data;\n if (data._meta_data) {\n const metaData =\n typeof data._meta_data === 'string'\n ? JSON.parse(data._meta_data)\n : data._meta_data;\n\n if (process.env.DEBUG_STI) {\n logger.debug('[formatDataJs] Merging _meta_data', {\n metaDataKeys: Object.keys(metaData),\n metaData,\n });\n }\n\n // Merge meta fields into a copy (will be formatted below). Spreading\n // `metaData` last preserves the original precedence of Object.assign.\n mergedData = { ...data, ...metaData };\n\n if (process.env.DEBUG_STI) {\n logger.debug('[formatDataJs] After merge, data keys', {\n keys: Object.keys(mergedData),\n });\n }\n }\n\n for (const [key, value] of Object.entries(mergedData)) {\n // Preserve keys with leading underscore (e.g., _meta_type, _meta_data)\n // These are special STI/framework fields that should not be camelCased\n const camelKey = key.startsWith('_') ? key : toCamelCase(key);\n\n // Determine the actual output key based on what's in fields\n // If the original key (snake_case) is in fields, use it; otherwise use camelCase\n // This supports both conventions (e.g., publish_date vs publishDate)\n let outputKey = camelKey;\n if (fields && key in fields && !(camelKey in fields)) {\n // Original key exists in fields but camelCase doesn't - use original (snake_case)\n outputKey = key;\n }\n\n // Get field type from fields, trying both key variants\n const fieldDef = fields?.[outputKey] ?? fields?.[camelKey] ?? fields?.[key];\n const fieldType = fieldDef?.type?.toLowerCase();\n\n if (value instanceof Date) {\n normalizedData[outputKey] = value;\n } else if (typeof value === 'string') {\n // Use field definitions if available, otherwise fall back to name patterns\n const isDate = fieldType === 'datetime' || isDateField(key);\n\n if (isDate) {\n const parsedDate = value.trim() ? new Date(value) : null;\n normalizedData[outputKey] =\n parsedDate && !Number.isNaN(parsedDate.getTime())\n ? parsedDate\n : value;\n } else if (fieldType === 'json') {\n // Parse JSON strings back to objects\n try {\n normalizedData[outputKey] = JSON.parse(value);\n } catch {\n // Keep as string if parsing fails\n normalizedData[outputKey] = value;\n }\n } else if (fieldType === 'integer') {\n // Convert string numbers to integers for INTEGER fields\n // SQLite may return \"2.0\" as a string in some cases\n const parsed = Number.parseInt(value, 10);\n normalizedData[outputKey] = Number.isNaN(parsed) ? value : parsed;\n } else if (fieldType === 'real' || fieldType === 'decimal') {\n // Convert string numbers to floats for REAL/DECIMAL fields\n const parsed = Number.parseFloat(value);\n normalizedData[outputKey] = Number.isNaN(parsed) ? value : parsed;\n } else {\n normalizedData[outputKey] = value;\n }\n } else if (typeof value === 'number') {\n if (fieldType === 'boolean') {\n // Convert SQLite integers (0/1) to booleans for boolean fields\n normalizedData[outputKey] = value === 1;\n } else {\n // Pass through numeric values as-is\n // Note: In JavaScript, 2.0 === 2 so no conversion needed\n // Non-integers in INTEGER fields (e.g., 2.9) are kept to surface data issues\n normalizedData[outputKey] = value;\n }\n } else {\n normalizedData[outputKey] = value;\n }\n }\n\n if (process.env.DEBUG_STI) {\n logger.debug('[formatDataJs] Output normalizedData', {\n keys: Object.keys(normalizedData),\n metaType: normalizedData._meta_type,\n });\n }\n\n return normalizedData;\n}\n\n/**\n * Type guard to check if a value is a Field instance\n *\n * @deprecated Field helpers have been removed. This function always returns false.\n * @param value - Value to check\n * @returns Always false (Field class no longer exists)\n */\nexport function isFieldInstance(value: any): value is never {\n return false;\n}\n\n/**\n * Formats data for SQL by converting Date objects to ISO strings\n * and camelCase property names to snake_case column names\n *\n * @param data - Object with data to format (camelCase property names from JavaScript)\n * @returns Object with properly formatted values and snake_case column names for SQL\n */\nexport function formatDataSql(data: Record<string, any>) {\n const normalizedData: Record<string, any> = {};\n for (const [key, value] of Object.entries(data)) {\n // Convert camelCase to snake_case for SQL\n const snakeKey = toSnakeCase(key);\n\n // Field helpers removed - no need to extract values (deprecated code removed)\n if (value instanceof Date) {\n normalizedData[snakeKey] = value.toISOString(); // Postgres accepts ISO format with timezone\n } else {\n normalizedData[snakeKey] = value;\n }\n }\n return normalizedData;\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"utils.js","sources":["../src/utils.ts"],"sourcesContent":["import { createLogger } from '@happyvertical/logger';\nimport type { SmrtObject } from './object';\nimport { ObjectRegistry } from './registry';\nimport type { SmrtObjectConstructor } from './registry/types';\nimport {\n classnameToTablename,\n pluralize,\n toSnakeCase,\n} from './utils/naming.js';\n\n// formatDataJs' debug traces are gated by DEBUG_STI, so the level must allow\n// debug when it's set (a fixed 'info' would filter them out and break the flag).\nconst logger = createLogger({\n level: process.env.DEBUG_STI ? 'debug' : 'info',\n});\n\nexport { classnameToTablename, pluralize, toSnakeCase };\n\n/**\n * Converts a snake_case string to camelCase\n *\n * @param str - String in snake_case format\n * @returns String in camelCase format\n * @example\n * ```typescript\n * toCamelCase('meetings_url'); // 'meetingsUrl'\n * toCamelCase('created_at'); // 'createdAt'\n * toCamelCase('id'); // 'id'\n * ```\n */\nexport function toCamelCase(str: string): string {\n return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());\n}\n\n/**\n * Converts all keys in an object from camelCase to snake_case\n *\n * @param obj - Object with camelCase keys\n * @returns Object with snake_case keys\n */\nexport function keysToSnakeCase(\n obj: Record<string, unknown>,\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n result[toSnakeCase(key)] = value;\n }\n return result;\n}\n\n/**\n * Converts all keys in an object from snake_case to camelCase\n *\n * @param obj - Object with snake_case keys\n * @returns Object with camelCase keys\n */\nexport function keysToCamelCase(\n obj: Record<string, unknown>,\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n result[toCamelCase(key)] = value;\n }\n return result;\n}\n\n/**\n * Checks if a field name indicates a date field based on naming conventions\n *\n * Recognizes common date field patterns like '_at', '_date', and 'date'.\n * Used for automatic type inference during schema generation.\n *\n * @param key - Field name to check\n * @returns Boolean indicating if the field is likely a date field\n * @example\n * ```typescript\n * isDateField('created_at'); // true\n * isDateField('updated_date'); // true\n * isDateField('name'); // false\n * ```\n */\nexport function isDateField(key: string) {\n // Fallback pattern matching when schema is not available\n // Primary date detection should use schema field types\n return key.endsWith('_date') || key.endsWith('_at') || key === 'date';\n}\n\n/**\n * Converts a date string to a Date object\n *\n * @param date - Date as string or Date object\n * @returns Date object\n */\nexport function dateAsString(date: Date | string) {\n if (typeof date === 'string') {\n return new Date(date);\n }\n return date;\n}\n\n/**\n * Converts a Date object to an ISO string\n *\n * @param date - Date as Date object or string\n * @returns ISO date string or the original string\n */\nexport function dateAsObject(date: Date | string) {\n if (date instanceof Date) {\n return date.toISOString();\n }\n return date;\n}\n\n/**\n * Extracts field definitions from a class constructor\n *\n * Uses ObjectRegistry cached fields from AST manifest exclusively.\n * No runtime introspection fallback - classes must be decorated with @smrt()\n * for schema generation to work.\n *\n * @param ClassType - Class constructor to extract fields from\n * @param values - Optional values to set for the fields\n * @returns Object containing field definitions with names, types, and values\n * @throws {Error} If the class is not registered in ObjectRegistry\n * @example\n * ```typescript\n * @smrt()\n * class Product extends SmrtObject {\n * name: string = '';\n * price: number = 0.0;\n * }\n *\n * const fields = await fieldsFromClass(Product);\n * console.log(fields.name.type); // 'TEXT'\n * console.log(fields.price.type); // 'REAL'\n * ```\n */\nexport async function fieldsFromClass(\n ClassType: new (...args: never[]) => SmrtObject,\n values?: Record<string, unknown>,\n) {\n // `getClassByConstructor` expects the registry's `SmrtObjectConstructor`\n // (`new (...args: any[]) => SmrtObject`); the narrower `never[]` param type\n // is contravariantly assignable, so this widening cast is purely structural.\n const className =\n ObjectRegistry.getClassByConstructor(ClassType as SmrtObjectConstructor)\n ?.name || ClassType.name;\n // NEW: Use getAllFields() to include inherited fields from parent classes\n const cachedFields = await ObjectRegistry.getAllFields(className);\n\n // Phase 2: AST manifest only - no runtime introspection fallback\n if (cachedFields.size === 0) {\n // Return empty fields for unregistered classes (for backward compatibility)\n // generateSchema() will throw if it needs field definitions\n return {};\n }\n\n // Use cached field definitions from AST manifest\n const fields: Record<string, unknown> = {};\n\n // Add/override with fields from cached registry\n for (const [key, field] of cachedFields.entries()) {\n const meta = { ...(field._meta || {}) };\n delete meta.__smrtSystemField;\n\n fields[key] = {\n name: key,\n type: field.type || 'TEXT',\n _meta: meta,\n ...(values && key in values ? { value: values[key] } : {}),\n };\n }\n\n return fields;\n}\n\n// NOTE: generateSchema moved to schema/utils.ts\n// to prevent bundling Node.js-only code (SchemaGenerator with node:crypto) in browser builds.\n// Import from './schema/utils' in Node.js code that needs schema generation.\n\n/**\n * Returns the old (incorrect) pluralized table name for migration purposes.\n *\n * This function replicates the previous buggy behavior where 'y' → 'ies'\n * transformation was applied AFTER adding 's', resulting in incorrect\n * pluralization (e.g., 'currency' → 'currencys' instead of 'currencies').\n *\n * Use this to generate migration SQL that renames old tables to new names.\n *\n * @param className - Name of the class\n * @returns The incorrectly pluralized table name (old behavior)\n * @example\n * ```typescript\n * // Generate migration for a class\n * const oldName = legacyClassnameToTablename('Currency'); // 'currencys'\n * const newName = classnameToTablename('Currency'); // 'currencies'\n * console.log(`ALTER TABLE ${oldName} RENAME TO ${newName};`);\n * ```\n */\nexport function legacyClassnameToTablename(className: string): string {\n return className\n .replace(/([a-z])([A-Z])/g, '$1_$2')\n .toLowerCase()\n .replace(/([^s])$/, '$1s')\n .replace(/y$/, 'ies'); // This runs AFTER 's' is added, so it's a no-op for 'y' words\n}\n\n/**\n * Generates migration SQL for renaming tables from old to new naming convention.\n *\n * Returns an array of SQL statements to rename tables that were affected\n * by the pluralization bug (Issue #839).\n *\n * @param classNames - Array of class names to check for migration\n * @returns Array of SQL RENAME statements (empty if no changes needed)\n * @example\n * ```typescript\n * const migrations = generateTableRenameMigrations([\n * 'Currency',\n * 'JournalEntry',\n * 'Product' // Not affected\n * ]);\n * // Returns:\n * // [\n * // 'ALTER TABLE currencys RENAME TO currencies;',\n * // 'ALTER TABLE journal_entrys RENAME TO journal_entries;'\n * // ]\n * ```\n */\nexport function generateTableRenameMigrations(classNames: string[]): string[] {\n const migrations: string[] = [];\n\n for (const className of classNames) {\n const oldName = legacyClassnameToTablename(className);\n const newName = classnameToTablename(className);\n\n if (oldName !== newName) {\n migrations.push(`ALTER TABLE ${oldName} RENAME TO ${newName};`);\n }\n }\n\n return migrations;\n}\n\n/**\n * Generates a table name from a class constructor\n *\n * Checks for SMRT_TABLE_NAME static property first (set by @smrt() decorator),\n * which survives code minification. Falls back to deriving from ClassType.name\n * for backward compatibility.\n *\n * @param ClassType - Class constructor or function\n * @returns Pluralized snake_case table name\n * @example\n * ```typescript\n * // With @smrt() decorator (recommended)\n * @smrt()\n * class Product extends SmrtObject { }\n * tableNameFromClass(Product); // \"products\" (captured before minification)\n *\n * // Without decorator (fallback)\n * class Category extends SmrtObject { }\n * tableNameFromClass(Category); // \"categories\" (derived from runtime name)\n * ```\n */\nexport function tableNameFromClass(\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n ClassType: Function | (new (...args: never[]) => SmrtObject),\n) {\n // Check for SMRT_TABLE_NAME property set by @smrt() decorator (survives minification)\n if ('SMRT_TABLE_NAME' in ClassType) {\n // The `in` guard above proves the static decorator property exists; read it\n // through a typed shape rather than `any`.\n return (ClassType as { SMRT_TABLE_NAME: string }).SMRT_TABLE_NAME;\n }\n\n // Fallback: derive from class name (breaks with minification)\n const snakeCase = ClassType.name\n // Insert underscore between lower & upper case letters\n .replace(/([a-z])([A-Z])/g, '$1_$2')\n // Convert to lowercase\n .toLowerCase();\n\n return pluralize(snakeCase);\n}\n\n/**\n * Formats data for JavaScript by converting date strings to Date objects\n * and snake_case column names to camelCase properties\n *\n * Generic over the input row type `T` so callers preserve their (typically\n * loose) row typing across the hydration boundary — this matches the historic\n * behavior where a `Record<string, any>` / `any` row produced an equally loose\n * result. Keys are re-cased and values are type-converted at runtime; the\n * return is the same `Record` shape, so it is reasserted as `T` at this DB\n * boundary rather than widened to an unrelated type.\n *\n * @param data - Object with data to format (snake_case column names from DB)\n * @param fields - Optional field definitions to determine types (from fieldsFromClass)\n * @returns Object with properly typed values and camelCase property names for JavaScript\n */\nexport function formatDataJs<\n T extends Record<string, unknown> = Record<string, unknown>,\n>(data: T, fields?: Record<string, { type?: string }>): T {\n const normalizedData: Record<string, unknown> = {};\n\n if (process.env.DEBUG_STI) {\n logger.debug('[formatDataJs] Input data', {\n hasMetaData: !!data._meta_data,\n metaType: data._meta_type,\n keys: Object.keys(data),\n });\n }\n\n // STI: If _meta_data exists, merge it into a FRESH object first so meta\n // fields are available during formatting. This must never mutate the caller's\n // `data` argument — `formatDataJs` is a public export and external callers may\n // pass shared objects that would otherwise get silent field injection (#1378).\n let mergedData: Record<string, unknown> = data;\n if (data._meta_data) {\n // `_meta_data` is the STI meta column: a JSON string from the DB or an\n // already-parsed object. Either way it is an object of meta fields; type it\n // as such at this boundary so the merge/key-walk below stays sound.\n const metaData: Record<string, unknown> =\n typeof data._meta_data === 'string'\n ? JSON.parse(data._meta_data)\n : (data._meta_data as Record<string, unknown>);\n\n if (process.env.DEBUG_STI) {\n logger.debug('[formatDataJs] Merging _meta_data', {\n metaDataKeys: Object.keys(metaData),\n metaData,\n });\n }\n\n // Merge meta fields into a copy (will be formatted below). Spreading\n // `metaData` last preserves the original precedence of Object.assign.\n mergedData = { ...data, ...metaData };\n\n if (process.env.DEBUG_STI) {\n logger.debug('[formatDataJs] After merge, data keys', {\n keys: Object.keys(mergedData),\n });\n }\n }\n\n for (const [key, value] of Object.entries(mergedData)) {\n // Preserve keys with leading underscore (e.g., _meta_type, _meta_data)\n // These are special STI/framework fields that should not be camelCased\n const camelKey = key.startsWith('_') ? key : toCamelCase(key);\n\n // Determine the actual output key based on what's in fields\n // If the original key (snake_case) is in fields, use it; otherwise use camelCase\n // This supports both conventions (e.g., publish_date vs publishDate)\n let outputKey = camelKey;\n if (fields && key in fields && !(camelKey in fields)) {\n // Original key exists in fields but camelCase doesn't - use original (snake_case)\n outputKey = key;\n }\n\n // Get field type from fields, trying both key variants\n const fieldDef = fields?.[outputKey] ?? fields?.[camelKey] ?? fields?.[key];\n const fieldType = fieldDef?.type?.toLowerCase();\n\n if (value instanceof Date) {\n normalizedData[outputKey] = value;\n } else if (typeof value === 'string') {\n // Use field definitions if available, otherwise fall back to name patterns\n const isDate = fieldType === 'datetime' || isDateField(key);\n\n if (isDate) {\n const parsedDate = value.trim() ? new Date(value) : null;\n normalizedData[outputKey] =\n parsedDate && !Number.isNaN(parsedDate.getTime())\n ? parsedDate\n : value;\n } else if (fieldType === 'json') {\n // Parse JSON strings back to objects\n try {\n normalizedData[outputKey] = JSON.parse(value);\n } catch {\n // Keep as string if parsing fails\n normalizedData[outputKey] = value;\n }\n } else if (fieldType === 'integer') {\n // Convert string numbers to integers for INTEGER fields\n // SQLite may return \"2.0\" as a string in some cases\n const parsed = Number.parseInt(value, 10);\n normalizedData[outputKey] = Number.isNaN(parsed) ? value : parsed;\n } else if (fieldType === 'real' || fieldType === 'decimal') {\n // Convert string numbers to floats for REAL/DECIMAL fields\n const parsed = Number.parseFloat(value);\n normalizedData[outputKey] = Number.isNaN(parsed) ? value : parsed;\n } else {\n normalizedData[outputKey] = value;\n }\n } else if (typeof value === 'number') {\n if (fieldType === 'boolean') {\n // Convert SQLite integers (0/1) to booleans for boolean fields\n normalizedData[outputKey] = value === 1;\n } else {\n // Pass through numeric values as-is\n // Note: In JavaScript, 2.0 === 2 so no conversion needed\n // Non-integers in INTEGER fields (e.g., 2.9) are kept to surface data issues\n normalizedData[outputKey] = value;\n }\n } else {\n normalizedData[outputKey] = value;\n }\n }\n\n if (process.env.DEBUG_STI) {\n logger.debug('[formatDataJs] Output normalizedData', {\n keys: Object.keys(normalizedData),\n metaType: normalizedData._meta_type,\n });\n }\n\n // Reassert the re-cased/type-converted row as the caller's row type `T`.\n // The output is structurally a `Record<string, unknown>`; this boundary cast\n // preserves the historic loose-in/loose-out hydration contract without `any`.\n return normalizedData as T;\n}\n\n/**\n * Type guard to check if a value is a Field instance\n *\n * @deprecated Field helpers have been removed. This function always returns false.\n * @param value - Value to check\n * @returns Always false (Field class no longer exists)\n */\nexport function isFieldInstance(value: unknown): value is never {\n return false;\n}\n\n/**\n * Formats data for SQL by converting Date objects to ISO strings\n * and camelCase property names to snake_case column names\n *\n * @param data - Object with data to format (camelCase property names from JavaScript)\n * @returns Object with properly formatted values and snake_case column names for SQL\n */\nexport function formatDataSql(data: Record<string, unknown>) {\n const normalizedData: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(data)) {\n // Convert camelCase to snake_case for SQL\n const snakeKey = toSnakeCase(key);\n\n // Field helpers removed - no need to extract values (deprecated code removed)\n if (value instanceof Date) {\n normalizedData[snakeKey] = value.toISOString(); // Postgres accepts ISO format with timezone\n } else {\n normalizedData[snakeKey] = value;\n }\n }\n return normalizedData;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/vite-plugin/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EACV,qBAAqB,EAEtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,MAAM,EAAiC,MAAM,MAAM,CAAC;AAgBlE,YAAY,EACV,wBAAwB,EACxB,gBAAgB,GACjB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,6BAA6B,EAC7B,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,4BAA4B,GAC7B,MAAM,0BAA0B,CAAC;AAalC,MAAM,WAAW,iBAAiB;IAChC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,0BAA0B;IAC1B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,6BAA6B;IAC7B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qBAAqB;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,kFAAkF;IAClF,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,4EAA4E;IAC5E,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wEAAwE;IACxE,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpC;;OAEG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,8CAA8C;IAC9C,SAAS,CAAC,EAAE;QACV,yDAAyD;QACzD,OAAO,EAAE,OAAO,CAAC;QACjB,wEAAwE;QACxE,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,qEAAqE;QACrE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,mEAAmE;QACnE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,mDAAmD;QACnD,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB;;;;WAIG;QACH,WAAW,CAAC,EAAE,OAAO,CAAC;KACvB,CAAC;IACF,mEAAmE;IACnE,SAAS,CAAC,EAAE,qBAAqB,GAAG,KAAK,CAAC;IAC1C;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AAsBD,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,MAAM,CAw2BlE"}
|
|
@@ -169,11 +169,13 @@ function smrtPlugin(options = {}) {
|
|
|
169
169
|
function preserveKnowledgeGeneratedAt(outputPath, nextKnowledge) {
|
|
170
170
|
if (!existsSync(outputPath)) return nextKnowledge;
|
|
171
171
|
try {
|
|
172
|
-
const current = JSON.parse(
|
|
172
|
+
const current = JSON.parse(
|
|
173
|
+
readFileSync(outputPath, "utf8")
|
|
174
|
+
);
|
|
173
175
|
if (semanticKnowledgeJson(current) === semanticKnowledgeJson(nextKnowledge)) {
|
|
174
176
|
return {
|
|
175
177
|
...nextKnowledge,
|
|
176
|
-
generatedAt: current.generatedAt
|
|
178
|
+
generatedAt: current.generatedAt ?? nextKnowledge.generatedAt
|
|
177
179
|
};
|
|
178
180
|
}
|
|
179
181
|
} catch {
|
|
@@ -494,13 +496,15 @@ function smrtPlugin(options = {}) {
|
|
|
494
496
|
}
|
|
495
497
|
},
|
|
496
498
|
async closeBundle() {
|
|
497
|
-
if (!manifest || !config
|
|
499
|
+
if (!manifest || !config?.build?.lib) {
|
|
498
500
|
return;
|
|
499
501
|
}
|
|
500
502
|
try {
|
|
501
503
|
const { writeFileSync, mkdirSync } = await import("node:fs");
|
|
502
504
|
const { resolve, dirname: dirname2 } = await import("node:path");
|
|
503
|
-
const
|
|
505
|
+
const rollupOutput = config.build?.rollupOptions?.output;
|
|
506
|
+
const rollupOutputDir = Array.isArray(rollupOutput) ? void 0 : rollupOutput?.dir;
|
|
507
|
+
const outDir = rollupOutputDir || config.build?.outDir || "dist";
|
|
504
508
|
const manifestPath = resolve(projectRoot, outDir, "manifest.json");
|
|
505
509
|
const knowledgePath = resolve(
|
|
506
510
|
projectRoot,
|
|
@@ -603,8 +607,10 @@ function smrtPlugin(options = {}) {
|
|
|
603
607
|
dist: "../scanner.js"
|
|
604
608
|
});
|
|
605
609
|
const manifestGen = new ManifestGenerator();
|
|
610
|
+
manifestGen.normalizeReportTenantScope(newManifest);
|
|
606
611
|
manifestGen.injectTenantScopedFields(newManifest);
|
|
607
612
|
manifestGen.mergeInheritedFields(newManifest);
|
|
613
|
+
manifestGen.normalizeReportObjects(newManifest);
|
|
608
614
|
manifestGen.generateValidationRules(newManifest);
|
|
609
615
|
manifestGen.generateSchemas(newManifest);
|
|
610
616
|
manifestGen.assertTenantScopedSchemaContract(newManifest);
|
|
@@ -1352,9 +1358,7 @@ async function generateSchemaModule(manifest) {
|
|
|
1352
1358
|
packageName: manifest.packageName || "unknown",
|
|
1353
1359
|
schemas,
|
|
1354
1360
|
dependencies: Array.from(
|
|
1355
|
-
new Set(
|
|
1356
|
-
Object.values(schemas).flatMap((s) => s.dependencies || [])
|
|
1357
|
-
)
|
|
1361
|
+
new Set(Object.values(schemas).flatMap((s) => s.dependencies || []))
|
|
1358
1362
|
)
|
|
1359
1363
|
};
|
|
1360
1364
|
return `// Auto-generated schema manifest from SMRT objects
|