@warlock.js/cascade 5.1.0 → 5.2.3

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.
Files changed (99) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +4 -0
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/context/database-data-source-context.d.mts.map +1 -1
  5. package/esm/context/database-transaction-context.d.mts.map +1 -1
  6. package/esm/contracts/query-builder.contract.d.mts +13 -0
  7. package/esm/contracts/query-builder.contract.d.mts.map +1 -1
  8. package/esm/data-source/data-source-registry.d.mts.map +1 -1
  9. package/esm/data-source/data-source.d.mts.map +1 -1
  10. package/esm/database-dirty-tracker.d.mts.map +1 -1
  11. package/esm/drivers/mongodb/mongodb-blueprint.mjs.map +1 -1
  12. package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -1
  13. package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -1
  14. package/esm/drivers/mongodb/mongodb-id-generator.mjs.map +1 -1
  15. package/esm/drivers/mongodb/mongodb-migration-driver.d.mts.map +1 -1
  16. package/esm/drivers/mongodb/mongodb-migration-driver.mjs.map +1 -1
  17. package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -1
  18. package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -1
  19. package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -1
  20. package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -1
  21. package/esm/drivers/mongodb/mongodb-sync-adapter.mjs.map +1 -1
  22. package/esm/drivers/postgres/postgres-blueprint.d.mts.map +1 -1
  23. package/esm/drivers/postgres/postgres-blueprint.mjs.map +1 -1
  24. package/esm/drivers/postgres/postgres-dialect.mjs.map +1 -1
  25. package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -1
  26. package/esm/drivers/postgres/postgres-driver.mjs.map +1 -1
  27. package/esm/drivers/postgres/postgres-migration-driver.d.mts.map +1 -1
  28. package/esm/drivers/postgres/postgres-migration-driver.mjs.map +1 -1
  29. package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -1
  30. package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -1
  31. package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -1
  32. package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -1
  33. package/esm/drivers/postgres/postgres-sql-serializer.mjs.map +1 -1
  34. package/esm/drivers/postgres/postgres-sync-adapter.mjs.map +1 -1
  35. package/esm/events/model-events.d.mts.map +1 -1
  36. package/esm/events/model-events.mjs.map +1 -1
  37. package/esm/expressions/column-expressions.d.mts.map +1 -1
  38. package/esm/index.d.mts +2 -2
  39. package/esm/migration/column-builder.d.mts.map +1 -1
  40. package/esm/migration/column-helpers.d.mts.map +1 -1
  41. package/esm/migration/foreign-key-builder.d.mts.map +1 -1
  42. package/esm/migration/migration-runner.d.mts +23 -3
  43. package/esm/migration/migration-runner.d.mts.map +1 -1
  44. package/esm/migration/migration-runner.mjs.map +1 -1
  45. package/esm/migration/migration.d.mts +1 -1
  46. package/esm/migration/migration.d.mts.map +1 -1
  47. package/esm/migration/migration.mjs.map +1 -1
  48. package/esm/migration/sql-grammar.mjs.map +1 -1
  49. package/esm/model/methods/accessor-methods.mjs.map +1 -1
  50. package/esm/model/methods/delete-methods.mjs.map +1 -1
  51. package/esm/model/methods/hydration-methods.mjs.map +1 -1
  52. package/esm/model/methods/instance-event-methods.mjs.map +1 -1
  53. package/esm/model/methods/meta-methods.mjs.map +1 -1
  54. package/esm/model/methods/pivot-methods.mjs.map +1 -1
  55. package/esm/model/methods/query-methods.mjs.map +1 -1
  56. package/esm/model/methods/restore-methods.mjs.map +1 -1
  57. package/esm/model/methods/scope-methods.mjs.map +1 -1
  58. package/esm/model/methods/serialization-methods.mjs.map +1 -1
  59. package/esm/model/methods/static-event-methods.mjs.map +1 -1
  60. package/esm/model/methods/write-methods.mjs.map +1 -1
  61. package/esm/model/model.d.mts.map +1 -1
  62. package/esm/model/model.mjs.map +1 -1
  63. package/esm/model/register-model.d.mts.map +1 -1
  64. package/esm/model/register-model.mjs.map +1 -1
  65. package/esm/operations/database.mjs.map +1 -1
  66. package/esm/operations/migrations.d.mts.map +1 -1
  67. package/esm/operations/migrations.mjs.map +1 -1
  68. package/esm/query-builder/query-builder.d.mts.map +1 -1
  69. package/esm/query-builder/query-builder.mjs.map +1 -1
  70. package/esm/relations/key-conventions.mjs.map +1 -1
  71. package/esm/relations/pivot-operations.d.mts.map +1 -1
  72. package/esm/relations/pivot-operations.mjs.map +1 -1
  73. package/esm/relations/relation-loader.d.mts.map +1 -1
  74. package/esm/relations/relation-loader.mjs.map +1 -1
  75. package/esm/remover/database-remover.mjs.map +1 -1
  76. package/esm/restorer/database-restorer.mjs.map +1 -1
  77. package/esm/sync/model-sync-operation.d.mts.map +1 -1
  78. package/esm/sync/model-sync-operation.mjs.map +1 -1
  79. package/esm/sync/model-sync.d.mts.map +1 -1
  80. package/esm/sync/sync-manager.mjs.map +1 -1
  81. package/esm/utils/connect-to-database.d.mts.map +1 -1
  82. package/esm/utils/connect-to-database.mjs.map +1 -1
  83. package/esm/utils/define-model.d.mts.map +1 -1
  84. package/esm/utils/escape-regex.mjs.map +1 -1
  85. package/esm/utils/is-valid-date-value.mjs.map +1 -1
  86. package/esm/utils/sanitize-filter.d.mts.map +1 -1
  87. package/esm/validation/database-writer-validation-error.d.mts.map +1 -1
  88. package/esm/validation/database-writer-validation-error.mjs.map +1 -1
  89. package/esm/validation/mutators/embed-mutator.mjs.map +1 -1
  90. package/esm/validation/plugins/embed-validator-plugin.mjs.map +1 -1
  91. package/esm/validation/rules/database-model-rule.mjs.map +1 -1
  92. package/esm/validation/rules/exists-rule.mjs.map +1 -1
  93. package/esm/validation/rules/unique-rule.mjs.map +1 -1
  94. package/esm/writer/database-writer.d.mts.map +1 -1
  95. package/esm/writer/database-writer.mjs.map +1 -1
  96. package/llms-full.txt +2 -0
  97. package/package.json +7 -4
  98. package/skills/cascade-basics/SKILL.md +3 -1
  99. package/skills/write-migration/SKILL.md +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"connect-to-database.mjs","names":[],"sources":["../../../../../../../cascade/src/utils/connect-to-database.ts"],"sourcesContent":["import { DriverContract, TransactionContext } from \"../contracts\";\nimport { DataSource } from \"../data-source/data-source\";\nimport { dataSourceRegistry } from \"../data-source/data-source-registry\";\nimport { MongoDbDriver } from \"../drivers/mongodb/mongodb-driver\";\nimport { PostgresDriver } from \"../drivers/postgres\";\nimport type { DeleteStrategy, MigrationDefaults, ModelDefaults, RelationDefaults } from \"../types\";\n\n/**\n * Supported database driver types.\n */\nexport type DatabaseDriver = \"mongodb\" | \"postgres\" | \"mysql\";\n\n/**\n * Default model configuration options.\n *\n * These settings will be applied to all models using this data source,\n * unless overridden by individual model static properties.\n *\n * This is a re-export of Partial<ModelDefaults> for backward compatibility\n * and to provide clearer naming in the connection config context.\n *\n * The full hierarchy is:\n * 1. Model static property (highest priority)\n * 2. Database config modelDefaults (this type)\n * 3. Driver defaults (e.g., snake_case for PostgreSQL, camelCase for MongoDB)\n * 4. Framework defaults (fallback values)\n *\n * @see ModelDefaults for complete type definition and documentation\n */\nexport type ModelDefaultConfig = Partial<ModelDefaults>;\n\n/**\n * Connection options for establishing a database connection.\n *\n * Generic type that separates concerns:\n * - Shared config (driver, name, database, connection details)\n * - Driver options (cascade-next driver-specific settings)\n * - Client options (native database client settings)\n * - Model options (default model behaviors)\n *\n * @template TDriverOptions - Driver-specific options (e.g., MongoDriverOptions)\n * @template TClientOptions - Native client options (e.g., MongoClientOptions from mongodb package)\n *\n * @example\n * ```typescript\n * // MongoDB\n * import type { MongoClientOptions } from \"mongodb\";\n * import type { MongoDriverOptions } from \"@warlock.js/cascade\";\n *\n * const config: ConnectionOptions<MongoDriverOptions, MongoClientOptions> = {\n * driver: \"mongodb\",\n * database: \"myapp\",\n * host: \"localhost\",\n * port: 27017,\n * driverOptions: {\n * autoGenerateId: true,\n * counterCollection: \"counters\",\n * },\n * clientOptions: {\n * minPoolSize: 5,\n * maxPoolSize: 10,\n * },\n * modelOptions: {\n * randomIncrement: true,\n * initialId: 1000,\n * },\n * };\n * ```\n */\nexport type ConnectionOptions<TDriverOptions = any, TClientOptions = any> = {\n // ============================================================================\n // SHARED CONFIGURATION (Framework-level)\n // ============================================================================\n\n /**\n * Database driver to use.\n * @default \"mongodb\"\n */\n driver?: DatabaseDriver;\n\n /**\n * Unique name for this data source.\n * Used for registration in DataSourceRegistry.\n * @default \"default\"\n */\n name?: string;\n\n /**\n * Whether this should be the default data source.\n * @default true\n */\n isDefault?: boolean;\n\n /**\n * Database name (required).\n */\n database: string;\n\n /**\n * Enable database operation logging (queries, execution time, parameters).\n * Highly recommended to keep this disabled in production to prevent sensitive data leakage.\n *\n * @default false\n */\n logging?: boolean;\n\n // ============================================================================\n // CONNECTION DETAILS (Shared across drivers)\n // ============================================================================\n\n /**\n * Database connection URI.\n * Alternative to specifying host/port separately.\n *\n * @example \"mongodb://localhost:27017/mydb\"\n * @example \"postgresql://user:pass@localhost:5432/mydb\"\n */\n uri?: string;\n\n /**\n * Database host.\n * @default \"localhost\"\n */\n host?: string;\n\n /**\n * Database port.\n * @default 27017 (MongoDB), 5432 (PostgreSQL)\n */\n port?: number;\n\n /**\n * Database username for authentication.\n */\n username?: string;\n\n /**\n * Database password for authentication.\n */\n password?: string;\n\n /**\n * Authentication source database.\n * Typically \"admin\" for MongoDB.\n */\n authSource?: string;\n\n // ============================================================================\n // DRIVER OPTIONS (Package-level, driver-specific)\n // ============================================================================\n\n /**\n * Driver-specific options.\n *\n * For MongoDB: { autoGenerateId, counterCollection, transactionOptions }\n * For PostgreSQL: { schema, ... }\n */\n driverOptions?: TDriverOptions;\n\n // ============================================================================\n // CLIENT OPTIONS (Native database client library)\n // ============================================================================\n\n /**\n * Native database client options.\n *\n * For MongoDB: MongoClientOptions from 'mongodb' package\n * For PostgreSQL: PoolConfig from 'pg' package\n */\n clientOptions?: TClientOptions;\n\n // ============================================================================\n // MODEL OPTIONS (Model defaults)\n // ============================================================================\n\n /**\n * Default model configuration for all models using this data source.\n *\n * These settings override driver defaults but are overridden by\n * individual model static properties.\n *\n * **Configuration Hierarchy (highest to lowest):**\n * 1. Model static property - `User.createdAtColumn = \"creation_date\"`\n * 2. **modelOptions (this)** - Database-wide overrides\n * 3. Driver defaults - PostgreSQL: snake_case, MongoDB: camelCase\n * 4. Framework defaults - Fallback values\n *\n * @example\n * ```typescript\n * // PostgreSQL database with custom settings\n * {\n * driver: \"postgres\",\n * modelOptions: {\n * // Override PostgreSQL default (snake_case) to use camelCase\n * namingConvention: \"camelCase\",\n * createdAtColumn: \"createdAt\",\n * updatedAtColumn: \"updatedAt\",\n *\n * // ID generation settings (for MongoDB)\n * randomIncrement: true,\n * initialId: 1000,\n *\n * // Deletion settings\n * deleteStrategy: \"soft\",\n * trashTable: \"archive\", // All models use same trash table\n * }\n * }\n *\n * // MongoDB database with defaults\n * {\n * driver: \"mongodb\",\n * modelOptions: {\n * // MongoDB already uses camelCase by default\n * randomIncrement: true,\n * initialId: 10000,\n * deleteStrategy: \"trash\", // Use RecycleBin\n * }\n * }\n * ```\n */\n modelOptions?: ModelDefaultConfig;\n\n /**\n * Migration-level defaults (UUID strategy, etc.).\n *\n * These defaults override driver migration defaults but can be\n * overridden by individual migration calls.\n *\n * @default undefined (uses driver defaults)\n *\n * @example\n * ```typescript\n * migrationDefaults: {\n * uuidStrategy: \"v7\", // Use UUID v7 for all migrations\n * }\n * ```\n */\n migrationOptions?: MigrationDefaults;\n\n /**\n * Defaults for relation conventions — foreign-key suffix and pivot-table\n * naming order. Controls how `@BelongsTo` / `@HasOne` / `@HasMany` /\n * `@BelongsToMany` infer column / table names when none are explicitly\n * configured on the decorator.\n *\n * @default undefined (uses framework defaults: `\"_id\"` suffix + alphabetical pivot)\n *\n * @example\n * ```typescript\n * relationOptions: {\n * foreignKeySuffix: \"_id\",\n * pivotTableNamingOrder: \"alphabetical\",\n * }\n * ```\n */\n relationOptions?: RelationDefaults;\n\n // ============================================================================\n // DATA SOURCE DEFAULTS\n // ============================================================================\n\n /**\n * Default delete strategy for models using this data source.\n *\n * - MongoDB: Typically `\"trash\"` (uses RecycleBin collection)\n * - PostgreSQL: Typically `\"permanent\"` or `\"soft\"`\n *\n * Can be overridden by model static property or destroy() options.\n *\n * @default undefined (falls back to \"permanent\")\n */\n defaultDeleteStrategy?: DeleteStrategy;\n\n /**\n * Default trash table/collection name for \"trash\" delete strategy.\n *\n * - MongoDB: Typically `\"RecycleBin\"`\n * - If not set, defaults to `{table}Trash` pattern\n *\n * Can be overridden by Model.trashTable static property.\n *\n * @default undefined (uses {table}Trash pattern)\n */\n defaultTrashTable?: string;\n\n // ============================================================================\n // MIGRATION OPTIONS\n // ============================================================================\n\n /**\n * Migration configuration options.\n */\n migrations?: {\n /**\n * Whether to wrap migrations in database transactions.\n *\n * Overrides driver defaults:\n * - PostgreSQL default: `true` (DDL is transactional)\n * - MongoDB default: `false` (DDL cannot be transactional)\n *\n * Individual migrations can override this with their own `transactional` property.\n *\n * @default undefined (uses driver default)\n */\n transactional?: boolean;\n\n /**\n * Name of the migrations tracking table/collection.\n *\n * @default \"_migrations\"\n */\n table?: string;\n };\n};\n\n/**\n * Connect to a database and register the data source.\n *\n * This is a high-level utility function that simplifies database connection\n * for small to medium projects. It handles driver instantiation, connection,\n * data source creation, and automatic registration.\n *\n * **Supported Drivers:**\n * - `mongodb` (default) - MongoDB driver with optional auto ID generation\n * - `postgres` - PostgreSQL driver (not yet implemented)\n * - `mysql` - MySQL driver (not yet implemented)\n *\n * **Features:**\n * - Automatic driver instantiation based on driver name\n * - Connection establishment and error handling\n * - DataSource creation and registration\n * - Support for MongoDB-specific features (ID generation, transactions)\n *\n * @param options - Connection configuration options\n * @returns A connected and registered DataSource instance\n * @throws {Error} If connection fails or driver is not implemented\n *\n * @example\n * ```typescript\n * // MongoDB with new structure\n * const dataSource = await connectToDatabase({\n * driver: \"mongodb\",\n * database: \"myapp\",\n * host: \"localhost\",\n * port: 27017,\n * driverOptions: {\n * autoGenerateId: true,\n * },\n * clientOptions: {\n * minPoolSize: 5,\n * maxPoolSize: 10,\n * },\n * modelOptions: {\n * randomIncrement: true,\n * initialId: 1000,\n * },\n * });\n * ```\n */\nexport async function connectToDatabase<TDriverOptions = any, TClientOptions = any>(\n options: ConnectionOptions<TDriverOptions, TClientOptions>,\n): Promise<DataSource> {\n // Default values\n const driverType = options.driver ?? \"mongodb\";\n const dataSourceName = options.name ?? \"default\";\n const isDefault = options.isDefault ?? true;\n\n // Create driver based on type\n let driver: DriverContract;\n\n switch (driverType) {\n case \"mongodb\": {\n driver = new MongoDbDriver(\n {\n database: options.database,\n uri: options.uri,\n host: options.host,\n port: options.port,\n username: options.username,\n password: options.password,\n authSource: options.authSource,\n logging: options.logging,\n clientOptions: options.clientOptions as any,\n },\n options.driverOptions as any,\n );\n break;\n }\n\n case \"postgres\": {\n driver = new PostgresDriver({\n database: options.database,\n connectionString: options.uri,\n host: options.host,\n port: options.port ?? 5432,\n user: options.username,\n password: options.password,\n logging: options.logging,\n // Spread any additional client options (pool settings, SSL, etc.)\n ...(options.clientOptions as object),\n });\n break;\n }\n\n case \"mysql\":\n throw new Error(\"MySQL driver is not yet implemented. Coming soon!\");\n\n default:\n throw new Error(\n `Unknown driver: \"${driverType}\". Supported drivers: mongodb, postgres, mysql`,\n );\n }\n\n // Create data source\n const dataSource = new DataSource({\n name: dataSourceName,\n driver,\n isDefault,\n defaultDeleteStrategy: options.defaultDeleteStrategy,\n defaultTrashTable: options.defaultTrashTable,\n modelDefaults: options.modelOptions,\n migrationDefaults: options.migrationOptions,\n relationDefaults: options.relationOptions,\n migrations: options.migrations,\n });\n\n // Register data source\n dataSourceRegistry.register(dataSource);\n\n // Connect to the database\n try {\n await driver.connect();\n } catch (error) {\n console.log(error);\n\n throw new Error(\n `Failed to connect to ${driverType} database: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n return dataSource;\n}\n\n/**\n * Get current driver instance.\n *\n * @example\n * ```typescript\n * const driver = getDatabaseDriver();\n *\n * // Pass type to return Postgres driver type\n * const pgDriver = getDatabaseDriver<PostgresDriver>();\n * ```\n */\nexport function getDatabaseDriver<T extends DriverContract = any>(): T {\n const driver = dataSourceRegistry.get().driver;\n\n return driver as unknown as T;\n}\n\n/**\n * Perform database transaction(s)\n * Shorthand to `dataSourceRegister.get().driver.transaction\n */\nexport async function transaction<T = any>(\n fn: (ctx: TransactionContext) => Promise<T>,\n options?: Record<string, unknown>,\n): Promise<T> {\n return getDatabaseDriver().transaction(fn, options);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuWA,eAAsB,kBACpB,SACqB;CAErB,MAAM,aAAa,QAAQ,UAAU;CACrC,MAAM,iBAAiB,QAAQ,QAAQ;CACvC,MAAM,YAAY,QAAQ,aAAa;CAGvC,IAAI;CAEJ,QAAQ,YAAR;EACE,KAAK;GACH,SAAS,IAAI,cACX;IACE,UAAU,QAAQ;IAClB,KAAK,QAAQ;IACb,MAAM,QAAQ;IACd,MAAM,QAAQ;IACd,UAAU,QAAQ;IAClB,UAAU,QAAQ;IAClB,YAAY,QAAQ;IACpB,SAAS,QAAQ;IACjB,eAAe,QAAQ;GACzB,GACA,QAAQ,aACV;GACA;EAGF,KAAK;GACH,SAAS,IAAI,eAAe;IAC1B,UAAU,QAAQ;IAClB,kBAAkB,QAAQ;IAC1B,MAAM,QAAQ;IACd,MAAM,QAAQ,QAAQ;IACtB,MAAM,QAAQ;IACd,UAAU,QAAQ;IAClB,SAAS,QAAQ;IAEjB,GAAI,QAAQ;GACd,CAAC;GACD;EAGF,KAAK,SACH,MAAM,IAAI,MAAM,mDAAmD;EAErE,SACE,MAAM,IAAI,MACR,oBAAoB,WAAW,+CACjC;CACJ;CAGA,MAAM,aAAa,IAAI,WAAW;EAChC,MAAM;EACN;EACA;EACA,uBAAuB,QAAQ;EAC/B,mBAAmB,QAAQ;EAC3B,eAAe,QAAQ;EACvB,mBAAmB,QAAQ;EAC3B,kBAAkB,QAAQ;EAC1B,YAAY,QAAQ;CACtB,CAAC;CAGD,mBAAmB,SAAS,UAAU;CAGtC,IAAI;EACF,MAAM,OAAO,QAAQ;CACvB,SAAS,OAAO;EACd,QAAQ,IAAI,KAAK;EAEjB,MAAM,IAAI,MACR,wBAAwB,WAAW,aAAa,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GACvG;CACF;CAEA,OAAO;AACT;;;;;;;;;;;;AAaA,SAAgB,oBAAuD;CAGrE,OAFe,mBAAmB,IAAI,CAAC,CAAC;AAG1C;;;;;AAMA,eAAsB,YACpB,IACA,SACY;CACZ,OAAO,kBAAkB,CAAC,CAAC,YAAY,IAAI,OAAO;AACpD"}
1
+ {"version":3,"file":"connect-to-database.mjs","names":[],"sources":["../../../../../../../cascade/src/utils/connect-to-database.ts"],"sourcesContent":["import { type DriverContract, type TransactionContext } from \"../contracts\";\nimport { DataSource } from \"../data-source/data-source\";\nimport { dataSourceRegistry } from \"../data-source/data-source-registry\";\nimport { MongoDbDriver } from \"../drivers/mongodb/mongodb-driver\";\nimport { PostgresDriver } from \"../drivers/postgres\";\nimport type { DeleteStrategy, MigrationDefaults, ModelDefaults, RelationDefaults } from \"../types\";\n\n/**\n * Supported database driver types.\n */\nexport type DatabaseDriver = \"mongodb\" | \"postgres\" | \"mysql\";\n\n/**\n * Default model configuration options.\n *\n * These settings will be applied to all models using this data source,\n * unless overridden by individual model static properties.\n *\n * This is a re-export of Partial<ModelDefaults> for backward compatibility\n * and to provide clearer naming in the connection config context.\n *\n * The full hierarchy is:\n * 1. Model static property (highest priority)\n * 2. Database config modelDefaults (this type)\n * 3. Driver defaults (e.g., snake_case for PostgreSQL, camelCase for MongoDB)\n * 4. Framework defaults (fallback values)\n *\n * @see ModelDefaults for complete type definition and documentation\n */\nexport type ModelDefaultConfig = Partial<ModelDefaults>;\n\n/**\n * Connection options for establishing a database connection.\n *\n * Generic type that separates concerns:\n * - Shared config (driver, name, database, connection details)\n * - Driver options (cascade-next driver-specific settings)\n * - Client options (native database client settings)\n * - Model options (default model behaviors)\n *\n * @template TDriverOptions - Driver-specific options (e.g., MongoDriverOptions)\n * @template TClientOptions - Native client options (e.g., MongoClientOptions from mongodb package)\n *\n * @example\n * ```typescript\n * // MongoDB\n * import type { MongoClientOptions } from \"mongodb\";\n * import type { MongoDriverOptions } from \"@warlock.js/cascade\";\n *\n * const config: ConnectionOptions<MongoDriverOptions, MongoClientOptions> = {\n * driver: \"mongodb\",\n * database: \"myapp\",\n * host: \"localhost\",\n * port: 27017,\n * driverOptions: {\n * autoGenerateId: true,\n * counterCollection: \"counters\",\n * },\n * clientOptions: {\n * minPoolSize: 5,\n * maxPoolSize: 10,\n * },\n * modelOptions: {\n * randomIncrement: true,\n * initialId: 1000,\n * },\n * };\n * ```\n */\nexport type ConnectionOptions<TDriverOptions = any, TClientOptions = any> = {\n // ============================================================================\n // SHARED CONFIGURATION (Framework-level)\n // ============================================================================\n\n /**\n * Database driver to use.\n * @default \"mongodb\"\n */\n driver?: DatabaseDriver;\n\n /**\n * Unique name for this data source.\n * Used for registration in DataSourceRegistry.\n * @default \"default\"\n */\n name?: string;\n\n /**\n * Whether this should be the default data source.\n * @default true\n */\n isDefault?: boolean;\n\n /**\n * Database name (required).\n */\n database: string;\n\n /**\n * Enable database operation logging (queries, execution time, parameters).\n * Highly recommended to keep this disabled in production to prevent sensitive data leakage.\n *\n * @default false\n */\n logging?: boolean;\n\n // ============================================================================\n // CONNECTION DETAILS (Shared across drivers)\n // ============================================================================\n\n /**\n * Database connection URI.\n * Alternative to specifying host/port separately.\n *\n * @example \"mongodb://localhost:27017/mydb\"\n * @example \"postgresql://user:pass@localhost:5432/mydb\"\n */\n uri?: string;\n\n /**\n * Database host.\n * @default \"localhost\"\n */\n host?: string;\n\n /**\n * Database port.\n * @default 27017 (MongoDB), 5432 (PostgreSQL)\n */\n port?: number;\n\n /**\n * Database username for authentication.\n */\n username?: string;\n\n /**\n * Database password for authentication.\n */\n password?: string;\n\n /**\n * Authentication source database.\n * Typically \"admin\" for MongoDB.\n */\n authSource?: string;\n\n // ============================================================================\n // DRIVER OPTIONS (Package-level, driver-specific)\n // ============================================================================\n\n /**\n * Driver-specific options.\n *\n * For MongoDB: { autoGenerateId, counterCollection, transactionOptions }\n * For PostgreSQL: { schema, ... }\n */\n driverOptions?: TDriverOptions;\n\n // ============================================================================\n // CLIENT OPTIONS (Native database client library)\n // ============================================================================\n\n /**\n * Native database client options.\n *\n * For MongoDB: MongoClientOptions from 'mongodb' package\n * For PostgreSQL: PoolConfig from 'pg' package\n */\n clientOptions?: TClientOptions;\n\n // ============================================================================\n // MODEL OPTIONS (Model defaults)\n // ============================================================================\n\n /**\n * Default model configuration for all models using this data source.\n *\n * These settings override driver defaults but are overridden by\n * individual model static properties.\n *\n * **Configuration Hierarchy (highest to lowest):**\n * 1. Model static property - `User.createdAtColumn = \"creation_date\"`\n * 2. **modelOptions (this)** - Database-wide overrides\n * 3. Driver defaults - PostgreSQL: snake_case, MongoDB: camelCase\n * 4. Framework defaults - Fallback values\n *\n * @example\n * ```typescript\n * // PostgreSQL database with custom settings\n * {\n * driver: \"postgres\",\n * modelOptions: {\n * // Override PostgreSQL default (snake_case) to use camelCase\n * namingConvention: \"camelCase\",\n * createdAtColumn: \"createdAt\",\n * updatedAtColumn: \"updatedAt\",\n *\n * // ID generation settings (for MongoDB)\n * randomIncrement: true,\n * initialId: 1000,\n *\n * // Deletion settings\n * deleteStrategy: \"soft\",\n * trashTable: \"archive\", // All models use same trash table\n * }\n * }\n *\n * // MongoDB database with defaults\n * {\n * driver: \"mongodb\",\n * modelOptions: {\n * // MongoDB already uses camelCase by default\n * randomIncrement: true,\n * initialId: 10000,\n * deleteStrategy: \"trash\", // Use RecycleBin\n * }\n * }\n * ```\n */\n modelOptions?: ModelDefaultConfig;\n\n /**\n * Migration-level defaults (UUID strategy, etc.).\n *\n * These defaults override driver migration defaults but can be\n * overridden by individual migration calls.\n *\n * @default undefined (uses driver defaults)\n *\n * @example\n * ```typescript\n * migrationDefaults: {\n * uuidStrategy: \"v7\", // Use UUID v7 for all migrations\n * }\n * ```\n */\n migrationOptions?: MigrationDefaults;\n\n /**\n * Defaults for relation conventions — foreign-key suffix and pivot-table\n * naming order. Controls how `@BelongsTo` / `@HasOne` / `@HasMany` /\n * `@BelongsToMany` infer column / table names when none are explicitly\n * configured on the decorator.\n *\n * @default undefined (uses framework defaults: `\"_id\"` suffix + alphabetical pivot)\n *\n * @example\n * ```typescript\n * relationOptions: {\n * foreignKeySuffix: \"_id\",\n * pivotTableNamingOrder: \"alphabetical\",\n * }\n * ```\n */\n relationOptions?: RelationDefaults;\n\n // ============================================================================\n // DATA SOURCE DEFAULTS\n // ============================================================================\n\n /**\n * Default delete strategy for models using this data source.\n *\n * - MongoDB: Typically `\"trash\"` (uses RecycleBin collection)\n * - PostgreSQL: Typically `\"permanent\"` or `\"soft\"`\n *\n * Can be overridden by model static property or destroy() options.\n *\n * @default undefined (falls back to \"permanent\")\n */\n defaultDeleteStrategy?: DeleteStrategy;\n\n /**\n * Default trash table/collection name for \"trash\" delete strategy.\n *\n * - MongoDB: Typically `\"RecycleBin\"`\n * - If not set, defaults to `{table}Trash` pattern\n *\n * Can be overridden by Model.trashTable static property.\n *\n * @default undefined (uses {table}Trash pattern)\n */\n defaultTrashTable?: string;\n\n // ============================================================================\n // MIGRATION OPTIONS\n // ============================================================================\n\n /**\n * Migration configuration options.\n */\n migrations?: {\n /**\n * Whether to wrap migrations in database transactions.\n *\n * Overrides driver defaults:\n * - PostgreSQL default: `true` (DDL is transactional)\n * - MongoDB default: `false` (DDL cannot be transactional)\n *\n * Individual migrations can override this with their own `transactional` property.\n *\n * @default undefined (uses driver default)\n */\n transactional?: boolean;\n\n /**\n * Name of the migrations tracking table/collection.\n *\n * @default \"_migrations\"\n */\n table?: string;\n };\n};\n\n/**\n * Connect to a database and register the data source.\n *\n * This is a high-level utility function that simplifies database connection\n * for small to medium projects. It handles driver instantiation, connection,\n * data source creation, and automatic registration.\n *\n * **Supported Drivers:**\n * - `mongodb` (default) - MongoDB driver with optional auto ID generation\n * - `postgres` - PostgreSQL driver (not yet implemented)\n * - `mysql` - MySQL driver (not yet implemented)\n *\n * **Features:**\n * - Automatic driver instantiation based on driver name\n * - Connection establishment and error handling\n * - DataSource creation and registration\n * - Support for MongoDB-specific features (ID generation, transactions)\n *\n * @param options - Connection configuration options\n * @returns A connected and registered DataSource instance\n * @throws {Error} If connection fails or driver is not implemented\n *\n * @example\n * ```typescript\n * // MongoDB with new structure\n * const dataSource = await connectToDatabase({\n * driver: \"mongodb\",\n * database: \"myapp\",\n * host: \"localhost\",\n * port: 27017,\n * driverOptions: {\n * autoGenerateId: true,\n * },\n * clientOptions: {\n * minPoolSize: 5,\n * maxPoolSize: 10,\n * },\n * modelOptions: {\n * randomIncrement: true,\n * initialId: 1000,\n * },\n * });\n * ```\n */\nexport async function connectToDatabase<TDriverOptions = any, TClientOptions = any>(\n options: ConnectionOptions<TDriverOptions, TClientOptions>,\n): Promise<DataSource> {\n // Default values\n const driverType = options.driver ?? \"mongodb\";\n const dataSourceName = options.name ?? \"default\";\n const isDefault = options.isDefault ?? true;\n\n // Create driver based on type\n let driver: DriverContract;\n\n switch (driverType) {\n case \"mongodb\": {\n driver = new MongoDbDriver(\n {\n database: options.database,\n uri: options.uri,\n host: options.host,\n port: options.port,\n username: options.username,\n password: options.password,\n authSource: options.authSource,\n logging: options.logging,\n clientOptions: options.clientOptions as any,\n },\n options.driverOptions as any,\n );\n break;\n }\n\n case \"postgres\": {\n driver = new PostgresDriver({\n database: options.database,\n connectionString: options.uri,\n host: options.host,\n port: options.port ?? 5432,\n user: options.username,\n password: options.password,\n logging: options.logging,\n // Spread any additional client options (pool settings, SSL, etc.)\n ...(options.clientOptions as object),\n });\n break;\n }\n\n case \"mysql\":\n throw new Error(\"MySQL driver is not yet implemented. Coming soon!\");\n\n default:\n throw new Error(\n `Unknown driver: \"${driverType}\". Supported drivers: mongodb, postgres, mysql`,\n );\n }\n\n // Create data source\n const dataSource = new DataSource({\n name: dataSourceName,\n driver,\n isDefault,\n defaultDeleteStrategy: options.defaultDeleteStrategy,\n defaultTrashTable: options.defaultTrashTable,\n modelDefaults: options.modelOptions,\n migrationDefaults: options.migrationOptions,\n relationDefaults: options.relationOptions,\n migrations: options.migrations,\n });\n\n // Register data source\n dataSourceRegistry.register(dataSource);\n\n // Connect to the database\n try {\n await driver.connect();\n } catch (error) {\n console.log(error);\n\n throw new Error(\n `Failed to connect to ${driverType} database: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n return dataSource;\n}\n\n/**\n * Get current driver instance.\n *\n * @example\n * ```typescript\n * const driver = getDatabaseDriver();\n *\n * // Pass type to return Postgres driver type\n * const pgDriver = getDatabaseDriver<PostgresDriver>();\n * ```\n */\nexport function getDatabaseDriver<T extends DriverContract = any>(): T {\n const driver = dataSourceRegistry.get().driver;\n\n return driver as unknown as T;\n}\n\n/**\n * Perform database transaction(s)\n * Shorthand to `dataSourceRegister.get().driver.transaction\n */\nexport async function transaction<T = any>(\n fn: (ctx: TransactionContext) => Promise<T>,\n options?: Record<string, unknown>,\n): Promise<T> {\n return getDatabaseDriver().transaction(fn, options);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuWA,eAAsB,kBACpB,SACqB;CAErB,MAAM,aAAa,QAAQ,UAAU;CACrC,MAAM,iBAAiB,QAAQ,QAAQ;CACvC,MAAM,YAAY,QAAQ,aAAa;CAGvC,IAAI;CAEJ,QAAQ,YAAR;EACE,KAAK;GACH,SAAS,IAAI,cACX;IACE,UAAU,QAAQ;IAClB,KAAK,QAAQ;IACb,MAAM,QAAQ;IACd,MAAM,QAAQ;IACd,UAAU,QAAQ;IAClB,UAAU,QAAQ;IAClB,YAAY,QAAQ;IACpB,SAAS,QAAQ;IACjB,eAAe,QAAQ;GACzB,GACA,QAAQ,aACV;GACA;EAGF,KAAK;GACH,SAAS,IAAI,eAAe;IAC1B,UAAU,QAAQ;IAClB,kBAAkB,QAAQ;IAC1B,MAAM,QAAQ;IACd,MAAM,QAAQ,QAAQ;IACtB,MAAM,QAAQ;IACd,UAAU,QAAQ;IAClB,SAAS,QAAQ;IAEjB,GAAI,QAAQ;GACd,CAAC;GACD;EAGF,KAAK,SACH,MAAM,IAAI,MAAM,mDAAmD;EAErE,SACE,MAAM,IAAI,MACR,oBAAoB,WAAW,+CACjC;CACJ;CAGA,MAAM,aAAa,IAAI,WAAW;EAChC,MAAM;EACN;EACA;EACA,uBAAuB,QAAQ;EAC/B,mBAAmB,QAAQ;EAC3B,eAAe,QAAQ;EACvB,mBAAmB,QAAQ;EAC3B,kBAAkB,QAAQ;EAC1B,YAAY,QAAQ;CACtB,CAAC;CAGD,mBAAmB,SAAS,UAAU;CAGtC,IAAI;EACF,MAAM,OAAO,QAAQ;CACvB,SAAS,OAAO;EACd,QAAQ,IAAI,KAAK;EAEjB,MAAM,IAAI,MACR,wBAAwB,WAAW,aAAa,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GACvG;CACF;CAEA,OAAO;AACT;;;;;;;;;;;;AAaA,SAAgB,oBAAuD;CAGrE,OAFe,mBAAmB,IAAI,EAAE;AAG1C;;;;;AAMA,eAAsB,YACpB,IACA,SACY;CACZ,OAAO,kBAAkB,EAAE,YAAY,IAAI,OAAO;AACpD"}
@@ -1 +1 @@
1
- {"version":3,"file":"define-model.d.mts","names":[],"sources":["../../../../../../../cascade/src/utils/define-model.ts"],"mappings":";;;;;;;;AASA;KAAY,kBAAA,iBAAmC,WAAA;EAAjB;;;EAI5B,KAAA;EAiCa;;;;EA3Bb,IAAA;EA2FU;;;;EArFV,MAAA,EAAQ,eAAA;EAZR;;;;;;;;;;EAwBA,cAAA,GAAiB,cAAA;EAuDjB;;;;;;;EA9CA,UAAA,GAAa,UAAA;EAgEG;AA2DlB;;;;;EAnHE,cAAA;EAsHoB;;;;;EA/GpB,eAAA;EAoH8B;;;;;EA7G9B,SAAA;EA8G4B;;;;;;;;;;;;;;;;;;;;;;EAtF5B,UAAA,GAAa,QAAA,CAAS,KAAA,CAAM,OAAA,KAAY,MAAA;EAiFvB;;;;;;;;;;;;;;;;EA/DjB,OAAA,GAAU,MAAA;AAAA;;;;;;;;;;;;;;;;;AAgJwE;AAwBpF;;;;;;;;;;;;;;;;;;;;AAIO;;;;;;;;;;;;;;;;;;;iBAjHS,WAAA,iBACE,WAAA,2BACS,eAAA,GAAkB,eAAA,sBACvB,MAAA,qCACH,MAAA,oBAEjB,OAAA,EAAS,kBAAA,CAAmB,OAAA;EAC1B,MAAA,EAAQ,gBAAA;EACR,UAAA,GAAa,QAAA,CAAS,KAAA,CAAM,KAAA,CAAM,MAAA,CAAO,gBAAA,MAAsB,WAAA;EAC/D,OAAA,GAAU,QAAA,QAAgB,KAAA,CAAM,KAAA,CAAM,MAAA,CAAO,gBAAA,MAAsB,QAAA;AAAA,UACpE,WAAA,GA2EoB,OAAA,CAAO,KAAA,CAAA,MAAA,CAAA,gBAAA,OAAmB,KAAA,CAAK,KAAA,CAAA,MAAA,CAAA,gBAAA,KAAmB,WAAA,IAAW,IAAA,QAAA,KAAA,WAAA,QAAA;;;;;;;;;;;;;;;;;;KAwBxE,SAAA,WAAoB,UAAA,QAAkB,WAAA,KAAgB,CAAA,kBAC7D,IAAA,uBAED,CAAA,SAAU,KAAA,YACR,CAAA"}
1
+ {"version":3,"file":"define-model.d.mts","names":[],"sources":["../../../../../../../cascade/src/utils/define-model.ts"],"mappings":";;;;;;;;AASA;KAAY,kBAAA,iBAAmC,WAAA;EAAjB;;;EAI5B,KAAA;EAiCa;;;;EA3Bb,IAAA;EA2FU;;;;EArFV,MAAA,EAAQ,eAAA;EAZR;;;;;;;;;;EAwBA,cAAA,GAAiB,cAAA;EAuDjB;;;;;;;EA9CA,UAAA,GAAa,UAAA;EAgEG;AA2DlB;;;;;EAnHE,cAAA;EAsHoB;;;;;EA/GpB,eAAA;EAoH8B;;;;;EA7G9B,SAAA;EA8G4B;;;;;;;;;;;;;;;;;;;;;;EAtF5B,UAAA,GAAa,QAAA,CAAS,KAAA,CAAM,OAAA,KAAY,MAAA;EAiFvB;;;;;;;;;;;;;;;;EA/DjB,OAAA,GAAU,MAAA;AAAA;;;;;;;;;;;;;;;;;AAgJwE;AAwBpF;;;;;;;;;;;;;;;;;;;;AAIO;;;;;;;;;;;;;;;;;;;iBAjHS,WAAA,iBACE,WAAA,2BACS,eAAA,GAAkB,eAAA,sBACvB,MAAA,qCACH,MAAA,mBAAA,CAEjB,OAAA,EAAS,kBAAA,CAAmB,OAAA;EAC1B,MAAA,EAAQ,gBAAA;EACR,UAAA,GAAa,QAAA,CAAS,KAAA,CAAM,KAAA,CAAM,MAAA,CAAO,gBAAA,MAAsB,WAAA;EAC/D,OAAA,GAAU,QAAA,QAAgB,KAAA,CAAM,KAAA,CAAM,MAAA,CAAO,gBAAA,MAAsB,QAAA;AAAA,UACpE,WAAA,GA2EoB,OAAA,CAAO,KAAA,CAAA,MAAA,CAAA,gBAAA,OAAmB,KAAA,CAAK,KAAA,CAAA,MAAA,CAAA,gBAAA,KAAmB,WAAA,IAAW,IAAA,QAAA,KAAA,WAAA,QAAA;;;;;;;;;;;;;;;;;;KAwBxE,SAAA,WAAoB,UAAA,QAAkB,WAAA,KAAgB,CAAA,kBAC7D,IAAA,uBAED,CAAA,SAAU,KAAA,YACR,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"escape-regex.mjs","names":[],"sources":["../../../../../../../cascade/src/utils/escape-regex.ts"],"sourcesContent":["/**\n * Regex escaping for pattern-matching helpers.\n *\n * `whereLike` / `whereNotLike` / `whereStartsWith` / `whereEndsWith` /\n * `whereSearch` compile their argument into a MongoDB `$regex`. The argument is\n * exactly what a search box hands over (`whereSearch(\"name\", req.query.q)`), so\n * passing it through unescaped gave the caller control of the regex itself:\n *\n * - **injection** — `.*` / `|` / `^` change the intended match semantics, and a\n * boolean-oracle probe (`^a`, `^b`, …) reads values back one character at a\n * time from a field the endpoint never meant to expose;\n * - **ReDoS** — nested quantifiers (`(a+)+`) backtrack catastrophically inside\n * `mongod`, against every document the query scans.\n *\n * A string value is therefore treated as a LITERAL. Only an explicit `RegExp`\n * argument — which cannot come from JSON, so it is developer-authored — reaches\n * the regex engine as a pattern.\n */\n\n/** Characters that carry meaning to the regex engine and must be neutralized. */\nconst REGEX_METACHARACTERS = /[.*+?^${}()|[\\]\\\\]/g;\n\n/**\n * Escape every regex metacharacter in a string so it matches itself.\n *\n * @param value - The literal text to match\n * @returns Regex source matching `value` verbatim\n *\n * @example\n * ```typescript\n * escapeRegex(\"(a+)+$\"); // \"\\\\(a\\\\+\\\\)\\\\+\\\\$\"\n * ```\n */\nexport function escapeRegex(value: string): string {\n return value.replace(REGEX_METACHARACTERS, \"\\\\$&\");\n}\n\n/**\n * Compile a `whereLike` pattern into regex source.\n *\n * The pattern is escaped first, so nothing the caller typed can act as a regex\n * operator; the SQL `LIKE` wildcard `%` is then translated to `.*` — the one\n * wildcard the API documents (`whereLike(\"email\", \"%@gmail.com\")`). Runs of `%`\n * collapse into a single `.*`, since `%%%%…` compiles to nothing but extra\n * backtracking work.\n *\n * The result stays unanchored: MongoDB's `whereLike` matches a substring\n * (`whereLike(\"name\", \"ar\")` finds \"Carol\"), which is the documented behavior of\n * this driver and is unchanged by the escaping.\n *\n * @param pattern - The user-supplied LIKE pattern\n * @returns Regex source matching the pattern literally, `%` aside\n *\n * @example\n * ```typescript\n * likePatternToRegexSource(\"%o'brien%\"); // \".*o'brien.*\"\n * likePatternToRegexSource(\"a.b\"); // \"a\\\\.b\" (a literal dot)\n * ```\n */\nexport function likePatternToRegexSource(pattern: string): string {\n return escapeRegex(pattern).replace(/%+/g, \".*\");\n}\n\n/**\n * Resolve a `whereLike`-style argument to regex source.\n *\n * An explicit `RegExp` is developer-authored and passes through as-is; a string\n * is user-shaped input and is treated as a literal LIKE pattern.\n *\n * @param pattern - A `RegExp` (trusted, used verbatim) or a string (escaped)\n * @returns Regex source ready for `$regex`\n */\nexport function resolveLikePattern(pattern: RegExp | string): string {\n return pattern instanceof RegExp ? pattern.source : likePatternToRegexSource(pattern);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAoBA,MAAM,uBAAuB;;;;;;;;;;;;AAa7B,SAAgB,YAAY,OAAuB;CACjD,OAAO,MAAM,QAAQ,sBAAsB,MAAM;AACnD;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,yBAAyB,SAAyB;CAChE,OAAO,YAAY,OAAO,CAAC,CAAC,QAAQ,OAAO,IAAI;AACjD;;;;;;;;;;AAWA,SAAgB,mBAAmB,SAAkC;CACnE,OAAO,mBAAmB,SAAS,QAAQ,SAAS,yBAAyB,OAAO;AACtF"}
1
+ {"version":3,"file":"escape-regex.mjs","names":[],"sources":["../../../../../../../cascade/src/utils/escape-regex.ts"],"sourcesContent":["/**\n * Regex escaping for pattern-matching helpers.\n *\n * `whereLike` / `whereNotLike` / `whereStartsWith` / `whereEndsWith` /\n * `whereSearch` compile their argument into a MongoDB `$regex`. The argument is\n * exactly what a search box hands over (`whereSearch(\"name\", req.query.q)`), so\n * passing it through unescaped gave the caller control of the regex itself:\n *\n * - **injection** — `.*` / `|` / `^` change the intended match semantics, and a\n * boolean-oracle probe (`^a`, `^b`, …) reads values back one character at a\n * time from a field the endpoint never meant to expose;\n * - **ReDoS** — nested quantifiers (`(a+)+`) backtrack catastrophically inside\n * `mongod`, against every document the query scans.\n *\n * A string value is therefore treated as a LITERAL. Only an explicit `RegExp`\n * argument — which cannot come from JSON, so it is developer-authored — reaches\n * the regex engine as a pattern.\n */\n\n/** Characters that carry meaning to the regex engine and must be neutralized. */\nconst REGEX_METACHARACTERS = /[.*+?^${}()|[\\]\\\\]/g;\n\n/**\n * Escape every regex metacharacter in a string so it matches itself.\n *\n * @param value - The literal text to match\n * @returns Regex source matching `value` verbatim\n *\n * @example\n * ```typescript\n * escapeRegex(\"(a+)+$\"); // \"\\\\(a\\\\+\\\\)\\\\+\\\\$\"\n * ```\n */\nexport function escapeRegex(value: string): string {\n return value.replace(REGEX_METACHARACTERS, \"\\\\$&\");\n}\n\n/**\n * Compile a `whereLike` pattern into regex source.\n *\n * The pattern is escaped first, so nothing the caller typed can act as a regex\n * operator; the SQL `LIKE` wildcard `%` is then translated to `.*` — the one\n * wildcard the API documents (`whereLike(\"email\", \"%@gmail.com\")`). Runs of `%`\n * collapse into a single `.*`, since `%%%%…` compiles to nothing but extra\n * backtracking work.\n *\n * The result stays unanchored: MongoDB's `whereLike` matches a substring\n * (`whereLike(\"name\", \"ar\")` finds \"Carol\"), which is the documented behavior of\n * this driver and is unchanged by the escaping.\n *\n * @param pattern - The user-supplied LIKE pattern\n * @returns Regex source matching the pattern literally, `%` aside\n *\n * @example\n * ```typescript\n * likePatternToRegexSource(\"%o'brien%\"); // \".*o'brien.*\"\n * likePatternToRegexSource(\"a.b\"); // \"a\\\\.b\" (a literal dot)\n * ```\n */\nexport function likePatternToRegexSource(pattern: string): string {\n return escapeRegex(pattern).replace(/%+/g, \".*\");\n}\n\n/**\n * Resolve a `whereLike`-style argument to regex source.\n *\n * An explicit `RegExp` is developer-authored and passes through as-is; a string\n * is user-shaped input and is treated as a literal LIKE pattern.\n *\n * @param pattern - A `RegExp` (trusted, used verbatim) or a string (escaped)\n * @returns Regex source ready for `$regex`\n */\nexport function resolveLikePattern(pattern: RegExp | string): string {\n return pattern instanceof RegExp ? pattern.source : likePatternToRegexSource(pattern);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAoBA,MAAM,uBAAuB;;;;;;;;;;;;AAa7B,SAAgB,YAAY,OAAuB;CACjD,OAAO,MAAM,QAAQ,sBAAsB,MAAM;AACnD;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,yBAAyB,SAAyB;CAChE,OAAO,YAAY,OAAO,EAAE,QAAQ,OAAO,IAAI;AACjD;;;;;;;;;;AAWA,SAAgB,mBAAmB,SAAkC;CACnE,OAAO,mBAAmB,SAAS,QAAQ,SAAS,yBAAyB,OAAO;AACtF"}
@@ -1 +1 @@
1
- {"version":3,"file":"is-valid-date-value.mjs","names":[],"sources":["../../../../../../../cascade/src/utils/is-valid-date-value.ts"],"sourcesContent":["const isoRegex = /^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?Z?)?$/;\n\n/**\n * Check if the given value is a valid date value\n */\nexport function isValidDateValue(value: unknown): boolean {\n // ✅ Handle timestamps\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) return false;\n\n const date = new Date(value);\n return !Number.isNaN(date.getTime());\n }\n\n // ❌ Only allow strict ISO strings\n if (typeof value !== \"string\") return false;\n\n if (!isoRegex.test(value)) return false;\n\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return false;\n\n // 🔥 Critical step: prevent JS auto-correction\n // Example: \"2023-02-31\" → March 3 (WRONG but \"valid\")\n const [y, m, d] = value.split(\"T\")[0].split(\"-\").map(Number);\n\n return date.getUTCFullYear() === y && date.getUTCMonth() + 1 === m && date.getUTCDate() === d;\n}\n"],"mappings":";AAAA,MAAM,WAAW;;;;AAKjB,SAAgB,iBAAiB,OAAyB;CAExD,IAAI,OAAO,UAAU,UAAU;EAC7B,IAAI,CAAC,OAAO,SAAS,KAAK,GAAG,OAAO;EAEpC,MAAM,OAAO,IAAI,KAAK,KAAK;EAC3B,OAAO,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC;CACrC;CAGA,IAAI,OAAO,UAAU,UAAU,OAAO;CAEtC,IAAI,CAAC,SAAS,KAAK,KAAK,GAAG,OAAO;CAElC,MAAM,OAAO,IAAI,KAAK,KAAK;CAC3B,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,GAAG,OAAO;CAIzC,MAAM,CAAC,GAAG,GAAG,KAAK,MAAM,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,MAAM;CAE3D,OAAO,KAAK,eAAe,MAAM,KAAK,KAAK,YAAY,IAAI,MAAM,KAAK,KAAK,WAAW,MAAM;AAC9F"}
1
+ {"version":3,"file":"is-valid-date-value.mjs","names":[],"sources":["../../../../../../../cascade/src/utils/is-valid-date-value.ts"],"sourcesContent":["const isoRegex = /^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?Z?)?$/;\n\n/**\n * Check if the given value is a valid date value\n */\nexport function isValidDateValue(value: unknown): boolean {\n // ✅ Handle timestamps\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) return false;\n\n const date = new Date(value);\n return !Number.isNaN(date.getTime());\n }\n\n // ❌ Only allow strict ISO strings\n if (typeof value !== \"string\") return false;\n\n if (!isoRegex.test(value)) return false;\n\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return false;\n\n // 🔥 Critical step: prevent JS auto-correction\n // Example: \"2023-02-31\" → March 3 (WRONG but \"valid\")\n const [y, m, d] = value.split(\"T\")[0].split(\"-\").map(Number);\n\n return date.getUTCFullYear() === y && date.getUTCMonth() + 1 === m && date.getUTCDate() === d;\n}\n"],"mappings":";AAAA,MAAM,WAAW;;;;AAKjB,SAAgB,iBAAiB,OAAyB;CAExD,IAAI,OAAO,UAAU,UAAU;EAC7B,IAAI,CAAC,OAAO,SAAS,KAAK,GAAG,OAAO;EAEpC,MAAM,OAAO,IAAI,KAAK,KAAK;EAC3B,OAAO,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC;CACrC;CAGA,IAAI,OAAO,UAAU,UAAU,OAAO;CAEtC,IAAI,CAAC,SAAS,KAAK,KAAK,GAAG,OAAO;CAElC,MAAM,OAAO,IAAI,KAAK,KAAK;CAC3B,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,GAAG,OAAO;CAIzC,MAAM,CAAC,GAAG,GAAG,KAAK,MAAM,MAAM,GAAG,EAAE,GAAG,MAAM,GAAG,EAAE,IAAI,MAAM;CAE3D,OAAO,KAAK,eAAe,MAAM,KAAK,KAAK,YAAY,IAAI,MAAM,KAAK,KAAK,WAAW,MAAM;AAC9F"}
@@ -1 +1 @@
1
- {"version":3,"file":"sanitize-filter.d.mts","names":[],"sources":["../../../../../../../cascade/src/utils/sanitize-filter.ts"],"mappings":";;AAkEA;;;;;;;;;iBAAgB,mBAAA,IAAuB,KAAA,EAAO,CAAA,EAAG,KAAA,WAAgB,CAAC;AAAA;AAelE;;;;;;;;;AAfkE,iBAelD,cAAA,WAAyB,MAAA,mBAAyB,MAAA,EAAQ,CAAA,GAAI,CAAA"}
1
+ {"version":3,"file":"sanitize-filter.d.mts","names":[],"sources":["../../../../../../../cascade/src/utils/sanitize-filter.ts"],"mappings":";;AAkEA;;;;;;;;;iBAAgB,mBAAA,GAAA,CAAuB,KAAA,EAAO,CAAA,EAAG,KAAA,WAAgB,CAAC;AAAA;AAelE;;;;;;;;;AAfkE,iBAelD,cAAA,WAAyB,MAAA,kBAAA,CAAyB,MAAA,EAAQ,CAAA,GAAI,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"database-writer-validation-error.d.mts","names":[],"sources":["../../../../../../../cascade/src/validation/database-writer-validation-error.ts"],"mappings":";;;;;AA0BA;;;;;;;;;;;;;;;;;;;;;cAAa,6BAAA,SAAsC,KAAA;EAsJX;AAAA;;;;;;;;EAAA,SA5ItB,MAAA,EAAQ,gBAAA;;;;;;;;;;;;;;cAeL,OAAA,UAAiB,MAAA,EAAQ,gBAAA;;;;;;;;;;;;;;;;;;EA0CrC,QAAA;;;;;;;;;;;;;;EAkEA,cAAA,CAAe,SAAA,WAAoB,gBAAA;;;;;;;;;;;;;;EAiBnC,aAAA,CAAc,SAAA;AAAA"}
1
+ {"version":3,"file":"database-writer-validation-error.d.mts","names":[],"sources":["../../../../../../../cascade/src/validation/database-writer-validation-error.ts"],"mappings":";;;;;AA0BA;;;;;;;;;;;;;;;;;;;;;cAAa,6BAAA,SAAsC,KAAA;EAsJX;AAAA;;;;;;;;EAAA,SA5ItB,MAAA,EAAQ,gBAAA;;;;;;;;;;;;;;cAeL,OAAA,UAAiB,MAAA,EAAQ,gBAAA;;;;;;;;;;;;;;;;;;EA0CrC,QAAA,CAAA;;;;;;;;;;;;;;EAkEA,cAAA,CAAe,SAAA,WAAoB,gBAAA;;;;;;;;;;;;;;EAiBnC,aAAA,CAAc,SAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"database-writer-validation-error.mjs","names":[],"sources":["../../../../../../../cascade/src/validation/database-writer-validation-error.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport type { ValidationResult } from \"@warlock.js/seal\";\n\n/**\n * Error thrown when model validation fails during database write operations.\n *\n * Contains detailed information about all validation errors,\n * including field paths, error messages, and validation rules that failed.\n *\n * @example\n * ```typescript\n * try {\n * const user = new User({ name: \"\", age: -5 });\n * await user.save();\n * } catch (error) {\n * if (error instanceof DatabaseWriterValidationError) {\n * console.log(error.message); // \"Validation failed\"\n * console.log(error.errors);\n * // [\n * // { path: \"name\", error: \"name is required\", rule: \"required\" },\n * // { path: \"age\", error: \"age must be at least 0\", rule: \"min\" }\n * // ]\n * }\n * }\n * ```\n */\nexport class DatabaseWriterValidationError extends Error {\n /**\n * Array of validation errors from @warlock.js/seal.\n *\n * Each error contains:\n * - `path`: Dot-notation path to the field (e.g., \"address.city\")\n * - `error`: Human-readable error message\n * - `rule`: The validation rule that failed (e.g., \"required\", \"email\")\n * - Additional context depending on the rule\n */\n public readonly errors: ValidationResult[\"errors\"];\n\n /**\n * Create a new DatabaseWriterValidationError.\n *\n * @param message - Error message (typically \"Validation failed\")\n * @param errors - Array of validation errors from seal\n *\n * @example\n * ```typescript\n * const error = new DatabaseWriterValidationError(\"Validation failed\", [\n * { path: \"email\", error: \"email must be valid\", rule: \"email\" }\n * ]);\n * ```\n */\n public constructor(message: string, errors: ValidationResult[\"errors\"]) {\n super(message);\n this.name = \"DatabaseWriterValidationError\";\n this.errors = errors;\n\n // Maintain proper stack trace for where error was thrown (V8 only)\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, DatabaseWriterValidationError);\n }\n\n // Override Node.js inspect to use our formatted output\n Object.defineProperty(this, \"inspect\", {\n value: () => this.toString(),\n enumerable: false,\n });\n }\n\n /**\n * Custom inspect method for Node.js console output.\n * This makes console.log and error logging use our beautiful format.\n */\n [Symbol.for(\"nodejs.util.inspect.custom\")](): string {\n return this.toString();\n }\n\n /**\n * Get a formatted string representation of all validation errors.\n *\n * Provides beautiful, colored terminal output with clear field-by-field breakdown.\n *\n * @returns Multi-line string with all errors, formatted for terminal\n *\n * @example\n * ```typescript\n * console.log(error.toString());\n * // ❌ Validation Error: UserModel\n * //\n * // Field: email\n * // Error: Email already exists\n * // Value: \"john@example.com\"\n * ```\n */\n public toString(): string {\n // Extract model name from message (e.g., \"[UserModel Model]\")\n const modelMatch = this.message.match(/\\[(\\w+)\\s+Model\\]/);\n const modelName = modelMatch ? modelMatch[1] : \"Model\";\n const operation = this.message.includes(\"Insert\") ? \"Insert\" : \"Update\";\n\n // Build header\n const lines: string[] = [];\n lines.push(\"\");\n lines.push(colors.red(`❌ Validation Error: ${modelName} (${operation})`));\n lines.push(\"\");\n\n // Group errors by field for better readability\n const errorsByField = new Map<string, Array<{ error: string; type?: string; value?: any }>>();\n\n for (const err of this.errors) {\n const fieldName = err.input || \"unknown\";\n if (!errorsByField.has(fieldName)) {\n errorsByField.set(fieldName, []);\n }\n errorsByField.get(fieldName)!.push({\n error: err.error,\n type: err.type,\n value: (err as any).value,\n });\n }\n\n // Format each field's errors\n for (const [fieldName, fieldErrors] of errorsByField) {\n lines.push(colors.yellow(` Field: ${fieldName}`));\n\n for (const fieldError of fieldErrors) {\n lines.push(colors.white(` Error: ${fieldError.error}`));\n\n if (fieldError.value !== undefined) {\n const valueStr =\n typeof fieldError.value === \"string\"\n ? `\"${fieldError.value}\"`\n : JSON.stringify(fieldError.value);\n lines.push(colors.gray(` Value: ${valueStr}`));\n }\n\n if (fieldError.type) {\n lines.push(colors.cyan(` Type: ${fieldError.type}`));\n }\n }\n\n lines.push(\"\"); // Blank line between fields\n }\n\n return lines.join(\"\\n\");\n }\n\n /**\n * Get validation errors for a specific field.\n *\n * @param fieldPath - Dot-notation path to the field\n * @returns Array of errors for that field\n *\n * @example\n * ```typescript\n * const emailErrors = error.getFieldErrors(\"email\");\n * console.log(emailErrors);\n * // [{ path: \"email\", error: \"email must be valid\", rule: \"email\" }]\n * ```\n */\n public getFieldErrors(fieldPath: string): ValidationResult[\"errors\"] {\n return this.errors.filter((err) => err.input === fieldPath);\n }\n\n /**\n * Check if a specific field has validation errors.\n *\n * @param fieldPath - Dot-notation path to the field\n * @returns True if the field has errors\n *\n * @example\n * ```typescript\n * if (error.hasFieldError(\"email\")) {\n * console.log(\"Email is invalid\");\n * }\n * ```\n */\n public hasFieldError(fieldPath: string): boolean {\n return this.errors.some((err) => err.input === fieldPath);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,IAAa,gCAAb,MAAa,sCAAsC,MAAM;;;;;;;;;;CAUvD,AAAgB;;;;;;;;;;;;;;CAehB,AAAO,YAAY,SAAiB,QAAoC;EACtE,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,SAAS;EAGd,IAAI,MAAM,mBACR,MAAM,kBAAkB,MAAM,6BAA6B;EAI7D,OAAO,eAAe,MAAM,WAAW;GACrC,aAAa,KAAK,SAAS;GAC3B,YAAY;EACd,CAAC;CACH;;;;;CAMA,CAAC,OAAO,IAAI,4BAA4B,KAAa;EACnD,OAAO,KAAK,SAAS;CACvB;;;;;;;;;;;;;;;;;;CAmBA,AAAO,WAAmB;EAExB,MAAM,aAAa,KAAK,QAAQ,MAAM,mBAAmB;EACzD,MAAM,YAAY,aAAa,WAAW,KAAK;EAC/C,MAAM,YAAY,KAAK,QAAQ,SAAS,QAAQ,IAAI,WAAW;EAG/D,MAAM,QAAkB,CAAC;EACzB,MAAM,KAAK,EAAE;EACb,MAAM,KAAK,OAAO,IAAI,uBAAuB,UAAU,IAAI,UAAU,EAAE,CAAC;EACxE,MAAM,KAAK,EAAE;EAGb,MAAM,gCAAgB,IAAI,IAAkE;EAE5F,KAAK,MAAM,OAAO,KAAK,QAAQ;GAC7B,MAAM,YAAY,IAAI,SAAS;GAC/B,IAAI,CAAC,cAAc,IAAI,SAAS,GAC9B,cAAc,IAAI,WAAW,CAAC,CAAC;GAEjC,cAAc,IAAI,SAAS,CAAC,CAAE,KAAK;IACjC,OAAO,IAAI;IACX,MAAM,IAAI;IACV,OAAQ,IAAY;GACtB,CAAC;EACH;EAGA,KAAK,MAAM,CAAC,WAAW,gBAAgB,eAAe;GACpD,MAAM,KAAK,OAAO,OAAO,YAAY,WAAW,CAAC;GAEjD,KAAK,MAAM,cAAc,aAAa;IACpC,MAAM,KAAK,OAAO,MAAM,YAAY,WAAW,OAAO,CAAC;IAEvD,IAAI,WAAW,UAAU,QAAW;KAClC,MAAM,WACJ,OAAO,WAAW,UAAU,WACxB,IAAI,WAAW,MAAM,KACrB,KAAK,UAAU,WAAW,KAAK;KACrC,MAAM,KAAK,OAAO,KAAK,YAAY,UAAU,CAAC;IAChD;IAEA,IAAI,WAAW,MACb,MAAM,KAAK,OAAO,KAAK,YAAY,WAAW,MAAM,CAAC;GAEzD;GAEA,MAAM,KAAK,EAAE;EACf;EAEA,OAAO,MAAM,KAAK,IAAI;CACxB;;;;;;;;;;;;;;CAeA,AAAO,eAAe,WAA+C;EACnE,OAAO,KAAK,OAAO,QAAQ,QAAQ,IAAI,UAAU,SAAS;CAC5D;;;;;;;;;;;;;;CAeA,AAAO,cAAc,WAA4B;EAC/C,OAAO,KAAK,OAAO,MAAM,QAAQ,IAAI,UAAU,SAAS;CAC1D;AACF"}
1
+ {"version":3,"file":"database-writer-validation-error.mjs","names":[],"sources":["../../../../../../../cascade/src/validation/database-writer-validation-error.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport type { ValidationResult } from \"@warlock.js/seal\";\n\n/**\n * Error thrown when model validation fails during database write operations.\n *\n * Contains detailed information about all validation errors,\n * including field paths, error messages, and validation rules that failed.\n *\n * @example\n * ```typescript\n * try {\n * const user = new User({ name: \"\", age: -5 });\n * await user.save();\n * } catch (error) {\n * if (error instanceof DatabaseWriterValidationError) {\n * console.log(error.message); // \"Validation failed\"\n * console.log(error.errors);\n * // [\n * // { path: \"name\", error: \"name is required\", rule: \"required\" },\n * // { path: \"age\", error: \"age must be at least 0\", rule: \"min\" }\n * // ]\n * }\n * }\n * ```\n */\nexport class DatabaseWriterValidationError extends Error {\n /**\n * Array of validation errors from @warlock.js/seal.\n *\n * Each error contains:\n * - `path`: Dot-notation path to the field (e.g., \"address.city\")\n * - `error`: Human-readable error message\n * - `rule`: The validation rule that failed (e.g., \"required\", \"email\")\n * - Additional context depending on the rule\n */\n public readonly errors: ValidationResult[\"errors\"];\n\n /**\n * Create a new DatabaseWriterValidationError.\n *\n * @param message - Error message (typically \"Validation failed\")\n * @param errors - Array of validation errors from seal\n *\n * @example\n * ```typescript\n * const error = new DatabaseWriterValidationError(\"Validation failed\", [\n * { path: \"email\", error: \"email must be valid\", rule: \"email\" }\n * ]);\n * ```\n */\n public constructor(message: string, errors: ValidationResult[\"errors\"]) {\n super(message);\n this.name = \"DatabaseWriterValidationError\";\n this.errors = errors;\n\n // Maintain proper stack trace for where error was thrown (V8 only)\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, DatabaseWriterValidationError);\n }\n\n // Override Node.js inspect to use our formatted output\n Object.defineProperty(this, \"inspect\", {\n value: () => this.toString(),\n enumerable: false,\n });\n }\n\n /**\n * Custom inspect method for Node.js console output.\n * This makes console.log and error logging use our beautiful format.\n */\n [Symbol.for(\"nodejs.util.inspect.custom\")](): string {\n return this.toString();\n }\n\n /**\n * Get a formatted string representation of all validation errors.\n *\n * Provides beautiful, colored terminal output with clear field-by-field breakdown.\n *\n * @returns Multi-line string with all errors, formatted for terminal\n *\n * @example\n * ```typescript\n * console.log(error.toString());\n * // ❌ Validation Error: UserModel\n * //\n * // Field: email\n * // Error: Email already exists\n * // Value: \"john@example.com\"\n * ```\n */\n public toString(): string {\n // Extract model name from message (e.g., \"[UserModel Model]\")\n const modelMatch = this.message.match(/\\[(\\w+)\\s+Model\\]/);\n const modelName = modelMatch ? modelMatch[1] : \"Model\";\n const operation = this.message.includes(\"Insert\") ? \"Insert\" : \"Update\";\n\n // Build header\n const lines: string[] = [];\n lines.push(\"\");\n lines.push(colors.red(`❌ Validation Error: ${modelName} (${operation})`));\n lines.push(\"\");\n\n // Group errors by field for better readability\n const errorsByField = new Map<string, Array<{ error: string; type?: string; value?: any }>>();\n\n for (const err of this.errors) {\n const fieldName = err.input || \"unknown\";\n if (!errorsByField.has(fieldName)) {\n errorsByField.set(fieldName, []);\n }\n errorsByField.get(fieldName)!.push({\n error: err.error,\n type: err.type,\n value: (err as any).value,\n });\n }\n\n // Format each field's errors\n for (const [fieldName, fieldErrors] of errorsByField) {\n lines.push(colors.yellow(` Field: ${fieldName}`));\n\n for (const fieldError of fieldErrors) {\n lines.push(colors.white(` Error: ${fieldError.error}`));\n\n if (fieldError.value !== undefined) {\n const valueStr =\n typeof fieldError.value === \"string\"\n ? `\"${fieldError.value}\"`\n : JSON.stringify(fieldError.value);\n lines.push(colors.gray(` Value: ${valueStr}`));\n }\n\n if (fieldError.type) {\n lines.push(colors.cyan(` Type: ${fieldError.type}`));\n }\n }\n\n lines.push(\"\"); // Blank line between fields\n }\n\n return lines.join(\"\\n\");\n }\n\n /**\n * Get validation errors for a specific field.\n *\n * @param fieldPath - Dot-notation path to the field\n * @returns Array of errors for that field\n *\n * @example\n * ```typescript\n * const emailErrors = error.getFieldErrors(\"email\");\n * console.log(emailErrors);\n * // [{ path: \"email\", error: \"email must be valid\", rule: \"email\" }]\n * ```\n */\n public getFieldErrors(fieldPath: string): ValidationResult[\"errors\"] {\n return this.errors.filter((err) => err.input === fieldPath);\n }\n\n /**\n * Check if a specific field has validation errors.\n *\n * @param fieldPath - Dot-notation path to the field\n * @returns True if the field has errors\n *\n * @example\n * ```typescript\n * if (error.hasFieldError(\"email\")) {\n * console.log(\"Email is invalid\");\n * }\n * ```\n */\n public hasFieldError(fieldPath: string): boolean {\n return this.errors.some((err) => err.input === fieldPath);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,IAAa,gCAAb,MAAa,sCAAsC,MAAM;;;;;;;;;;CAUvD,AAAgB;;;;;;;;;;;;;;CAehB,AAAO,YAAY,SAAiB,QAAoC;EACtE,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,SAAS;EAGd,IAAI,MAAM,mBACR,MAAM,kBAAkB,MAAM,6BAA6B;EAI7D,OAAO,eAAe,MAAM,WAAW;GACrC,aAAa,KAAK,SAAS;GAC3B,YAAY;EACd,CAAC;CACH;;;;;CAMA,CAAC,OAAO,IAAI,4BAA4B,KAAa;EACnD,OAAO,KAAK,SAAS;CACvB;;;;;;;;;;;;;;;;;;CAmBA,AAAO,WAAmB;EAExB,MAAM,aAAa,KAAK,QAAQ,MAAM,mBAAmB;EACzD,MAAM,YAAY,aAAa,WAAW,KAAK;EAC/C,MAAM,YAAY,KAAK,QAAQ,SAAS,QAAQ,IAAI,WAAW;EAG/D,MAAM,QAAkB,CAAC;EACzB,MAAM,KAAK,EAAE;EACb,MAAM,KAAK,OAAO,IAAI,uBAAuB,UAAU,IAAI,UAAU,EAAE,CAAC;EACxE,MAAM,KAAK,EAAE;EAGb,MAAM,gCAAgB,IAAI,IAAkE;EAE5F,KAAK,MAAM,OAAO,KAAK,QAAQ;GAC7B,MAAM,YAAY,IAAI,SAAS;GAC/B,IAAI,CAAC,cAAc,IAAI,SAAS,GAC9B,cAAc,IAAI,WAAW,CAAC,CAAC;GAEjC,cAAc,IAAI,SAAS,EAAG,KAAK;IACjC,OAAO,IAAI;IACX,MAAM,IAAI;IACV,OAAQ,IAAY;GACtB,CAAC;EACH;EAGA,KAAK,MAAM,CAAC,WAAW,gBAAgB,eAAe;GACpD,MAAM,KAAK,OAAO,OAAO,YAAY,WAAW,CAAC;GAEjD,KAAK,MAAM,cAAc,aAAa;IACpC,MAAM,KAAK,OAAO,MAAM,YAAY,WAAW,OAAO,CAAC;IAEvD,IAAI,WAAW,UAAU,QAAW;KAClC,MAAM,WACJ,OAAO,WAAW,UAAU,WACxB,IAAI,WAAW,MAAM,KACrB,KAAK,UAAU,WAAW,KAAK;KACrC,MAAM,KAAK,OAAO,KAAK,YAAY,UAAU,CAAC;IAChD;IAEA,IAAI,WAAW,MACb,MAAM,KAAK,OAAO,KAAK,YAAY,WAAW,MAAM,CAAC;GAEzD;GAEA,MAAM,KAAK,EAAE;EACf;EAEA,OAAO,MAAM,KAAK,IAAI;CACxB;;;;;;;;;;;;;;CAeA,AAAO,eAAe,WAA+C;EACnE,OAAO,KAAK,OAAO,QAAQ,QAAQ,IAAI,UAAU,SAAS;CAC5D;;;;;;;;;;;;;;CAeA,AAAO,cAAc,WAA4B;EAC/C,OAAO,KAAK,OAAO,MAAM,QAAQ,IAAI,UAAU,SAAS;CAC1D;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"embed-mutator.mjs","names":[],"sources":["../../../../../../../../cascade/src/validation/mutators/embed-mutator.ts"],"sourcesContent":["import type { Mutator } from \"@warlock.js/seal\";\nimport { ChildModel, Model } from \"../../model/model\";\nimport { getModelFromRegistry } from \"../../model/register-model\";\n\ntype DatabaseModelMutatorOptions = {\n model: ChildModel<any> | string;\n};\n\nexport const databaseModelMutator: Mutator<DatabaseModelMutatorOptions> = async (\n value,\n context,\n) => {\n let { model: ModelClass } = context?.options || {};\n\n if (typeof ModelClass === \"string\") {\n ModelClass = getModelFromRegistry(ModelClass)!;\n }\n\n if (!ModelClass) {\n throw new Error(`Model ${ModelClass} not found in registry`);\n }\n\n if (value instanceof Model) return value;\n\n if (typeof value === \"object\" && value?.id) {\n value = Number(value.id);\n }\n\n if (typeof value !== \"number\") return value;\n\n return await ModelClass.find(value);\n};\n\nexport const databaseModelsMutator: Mutator<DatabaseModelMutatorOptions> = async (\n value,\n context,\n) => {\n if (!Array.isArray(value)) return value;\n\n let { model: ModelClass } = context?.options || {};\n\n if (typeof ModelClass === \"string\") {\n ModelClass = getModelFromRegistry(ModelClass)!;\n }\n\n if (!ModelClass) {\n throw new Error(`Model ${ModelClass} not found in registry`);\n }\n\n // first, if all values are list of models, then return them.\n if (value.every((item) => item instanceof Model)) return value;\n\n const ids = value.map((item) => item?.id || item).filter((item) => item !== undefined);\n\n return await ModelClass.query().whereIn(\"id\", ids).get();\n};\n"],"mappings":";;;;AAQA,MAAa,uBAA6D,OACxE,OACA,YACG;CACH,IAAI,EAAE,OAAO,eAAe,SAAS,WAAW,CAAC;CAEjD,IAAI,OAAO,eAAe,UACxB,aAAa,qBAAqB,UAAU;CAG9C,IAAI,CAAC,YACH,MAAM,IAAI,MAAM,SAAS,WAAW,uBAAuB;CAG7D,IAAI,iBAAiB,OAAO,OAAO;CAEnC,IAAI,OAAO,UAAU,YAAY,OAAO,IACtC,QAAQ,OAAO,MAAM,EAAE;CAGzB,IAAI,OAAO,UAAU,UAAU,OAAO;CAEtC,OAAO,MAAM,WAAW,KAAK,KAAK;AACpC;AAEA,MAAa,wBAA8D,OACzE,OACA,YACG;CACH,IAAI,CAAC,MAAM,QAAQ,KAAK,GAAG,OAAO;CAElC,IAAI,EAAE,OAAO,eAAe,SAAS,WAAW,CAAC;CAEjD,IAAI,OAAO,eAAe,UACxB,aAAa,qBAAqB,UAAU;CAG9C,IAAI,CAAC,YACH,MAAM,IAAI,MAAM,SAAS,WAAW,uBAAuB;CAI7D,IAAI,MAAM,OAAO,SAAS,gBAAgB,KAAK,GAAG,OAAO;CAEzD,MAAM,MAAM,MAAM,KAAK,SAAS,MAAM,MAAM,IAAI,CAAC,CAAC,QAAQ,SAAS,SAAS,MAAS;CAErF,OAAO,MAAM,WAAW,MAAM,CAAC,CAAC,QAAQ,MAAM,GAAG,CAAC,CAAC,IAAI;AACzD"}
1
+ {"version":3,"file":"embed-mutator.mjs","names":[],"sources":["../../../../../../../../cascade/src/validation/mutators/embed-mutator.ts"],"sourcesContent":["import type { Mutator } from \"@warlock.js/seal\";\nimport { type ChildModel, Model } from \"../../model/model\";\nimport { getModelFromRegistry } from \"../../model/register-model\";\n\ntype DatabaseModelMutatorOptions = {\n model: ChildModel<any> | string;\n};\n\nexport const databaseModelMutator: Mutator<DatabaseModelMutatorOptions> = async (\n value,\n context,\n) => {\n let { model: ModelClass } = context?.options || {};\n\n if (typeof ModelClass === \"string\") {\n ModelClass = getModelFromRegistry(ModelClass)!;\n }\n\n if (!ModelClass) {\n throw new Error(`Model ${ModelClass} not found in registry`);\n }\n\n if (value instanceof Model) return value;\n\n if (typeof value === \"object\" && value?.id) {\n value = Number(value.id);\n }\n\n if (typeof value !== \"number\") return value;\n\n return await ModelClass.find(value);\n};\n\nexport const databaseModelsMutator: Mutator<DatabaseModelMutatorOptions> = async (\n value,\n context,\n) => {\n if (!Array.isArray(value)) return value;\n\n let { model: ModelClass } = context?.options || {};\n\n if (typeof ModelClass === \"string\") {\n ModelClass = getModelFromRegistry(ModelClass)!;\n }\n\n if (!ModelClass) {\n throw new Error(`Model ${ModelClass} not found in registry`);\n }\n\n // first, if all values are list of models, then return them.\n if (value.every((item) => item instanceof Model)) return value;\n\n const ids = value.map((item) => item?.id || item).filter((item) => item !== undefined);\n\n return await ModelClass.query().whereIn(\"id\", ids).get();\n};\n"],"mappings":";;;;AAQA,MAAa,uBAA6D,OACxE,OACA,YACG;CACH,IAAI,EAAE,OAAO,eAAe,SAAS,WAAW,CAAC;CAEjD,IAAI,OAAO,eAAe,UACxB,aAAa,qBAAqB,UAAU;CAG9C,IAAI,CAAC,YACH,MAAM,IAAI,MAAM,SAAS,WAAW,uBAAuB;CAG7D,IAAI,iBAAiB,OAAO,OAAO;CAEnC,IAAI,OAAO,UAAU,YAAY,OAAO,IACtC,QAAQ,OAAO,MAAM,EAAE;CAGzB,IAAI,OAAO,UAAU,UAAU,OAAO;CAEtC,OAAO,MAAM,WAAW,KAAK,KAAK;AACpC;AAEA,MAAa,wBAA8D,OACzE,OACA,YACG;CACH,IAAI,CAAC,MAAM,QAAQ,KAAK,GAAG,OAAO;CAElC,IAAI,EAAE,OAAO,eAAe,SAAS,WAAW,CAAC;CAEjD,IAAI,OAAO,eAAe,UACxB,aAAa,qBAAqB,UAAU;CAG9C,IAAI,CAAC,YACH,MAAM,IAAI,MAAM,SAAS,WAAW,uBAAuB;CAI7D,IAAI,MAAM,OAAO,SAAS,gBAAgB,KAAK,GAAG,OAAO;CAEzD,MAAM,MAAM,MAAM,KAAK,SAAS,MAAM,MAAM,IAAI,EAAE,QAAQ,SAAS,SAAS,MAAS;CAErF,OAAO,MAAM,WAAW,MAAM,EAAE,QAAQ,MAAM,GAAG,EAAE,IAAI;AACzD"}
@@ -1 +1 @@
1
- {"version":3,"file":"embed-validator-plugin.mjs","names":[],"sources":["../../../../../../../../cascade/src/validation/plugins/embed-validator-plugin.ts"],"sourcesContent":["/**\n * Embed Validator Plugin\n *\n * Adds embed validation to Seal v factory\n */\n\nimport type { SealPlugin } from \"@warlock.js/seal\";\nimport { v } from \"@warlock.js/seal\";\nimport type { ChildModel } from \"../../model/model\";\nimport { EmbedModelValidator } from \"../validators/embed-validator\";\n\ntype EmbedOptions = {\n errorMessage?: string;\n embed?: string | string[];\n};\n\ndeclare module \"@warlock.js/seal\" {\n interface ValidatorV {\n embed(model: ChildModel<any> | string, options?: EmbedOptions): EmbedModelValidator;\n embedMany(model: ChildModel<any> | string, options?: EmbedOptions): EmbedModelValidator;\n }\n}\n\n/**\n * File validation plugin for Seal\n */\nexport const embedValidator: SealPlugin = {\n name: \"embed\",\n version: \"1.0.0\",\n description: \"Adds embed validation (v.embed())\",\n\n install() {\n // Inject embed() method into v factory\n v.embed = (model: ChildModel<any> | string, options?: EmbedOptions) =>\n new EmbedModelValidator().model(model).embed(options?.embed);\n v.embedMany = (model: ChildModel<any> | string, options?: EmbedOptions) =>\n new EmbedModelValidator().models(model).embed(options?.embed);\n },\n};\n"],"mappings":";;;;;;;AA0BA,MAAa,iBAA6B;CACxC,MAAM;CACN,SAAS;CACT,aAAa;CAEb,UAAU;EAER,EAAE,SAAS,OAAiC,YAC1C,IAAI,oBAAoB,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,SAAS,KAAK;EAC7D,EAAE,aAAa,OAAiC,YAC9C,IAAI,oBAAoB,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,SAAS,KAAK;CAChE;AACF"}
1
+ {"version":3,"file":"embed-validator-plugin.mjs","names":[],"sources":["../../../../../../../../cascade/src/validation/plugins/embed-validator-plugin.ts"],"sourcesContent":["/**\n * Embed Validator Plugin\n *\n * Adds embed validation to Seal v factory\n */\n\nimport type { SealPlugin } from \"@warlock.js/seal\";\nimport { v } from \"@warlock.js/seal\";\nimport type { ChildModel } from \"../../model/model\";\nimport { EmbedModelValidator } from \"../validators/embed-validator\";\n\ntype EmbedOptions = {\n errorMessage?: string;\n embed?: string | string[];\n};\n\ndeclare module \"@warlock.js/seal\" {\n interface ValidatorV {\n embed(model: ChildModel<any> | string, options?: EmbedOptions): EmbedModelValidator;\n embedMany(model: ChildModel<any> | string, options?: EmbedOptions): EmbedModelValidator;\n }\n}\n\n/**\n * File validation plugin for Seal\n */\nexport const embedValidator: SealPlugin = {\n name: \"embed\",\n version: \"1.0.0\",\n description: \"Adds embed validation (v.embed())\",\n\n install() {\n // Inject embed() method into v factory\n v.embed = (model: ChildModel<any> | string, options?: EmbedOptions) =>\n new EmbedModelValidator().model(model).embed(options?.embed);\n v.embedMany = (model: ChildModel<any> | string, options?: EmbedOptions) =>\n new EmbedModelValidator().models(model).embed(options?.embed);\n },\n};\n"],"mappings":";;;;;;;AA0BA,MAAa,iBAA6B;CACxC,MAAM;CACN,SAAS;CACT,aAAa;CAEb,UAAU;EAER,EAAE,SAAS,OAAiC,YAC1C,IAAI,oBAAoB,EAAE,MAAM,KAAK,EAAE,MAAM,SAAS,KAAK;EAC7D,EAAE,aAAa,OAAiC,YAC9C,IAAI,oBAAoB,EAAE,OAAO,KAAK,EAAE,MAAM,SAAS,KAAK;CAChE;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"database-model-rule.mjs","names":[],"sources":["../../../../../../../../cascade/src/validation/rules/database-model-rule.ts"],"sourcesContent":["import { invalidRule, VALID_RULE, type SchemaRule } from \"@warlock.js/seal\";\r\nimport { ChildModel, Model } from \"./../../model/model\";\r\nimport { getModelFromRegistry } from \"./../../model/register-model\";\r\n\r\nexport const databaseModelRule: SchemaRule = {\r\n name: \"databaseModel\",\r\n defaultErrorMessage: \"The :input must be a valid :model model\",\r\n async validate(value, context) {\r\n if (value instanceof Model === false) {\r\n this.context.attributesList.model = this.context.options.model?.name;\r\n return invalidRule(this, context);\r\n }\r\n\r\n return VALID_RULE;\r\n },\r\n};\r\n\r\nexport const databaseModelsRule: SchemaRule<{ model: ChildModel<any> | string }> = {\r\n name: \"databaseModels\",\r\n defaultErrorMessage: \"The :input must be a list of valid :model\",\r\n async validate(value, context) {\r\n let { model } = this.context.options;\r\n if (typeof model === \"string\") {\r\n model = getModelFromRegistry(model)!;\r\n }\r\n\r\n this.context.attributesList.model = model.name;\r\n\r\n if (!Array.isArray(value)) return invalidRule(this, context);\r\n\r\n if (value.every((item) => item instanceof Model)) return VALID_RULE;\r\n\r\n return invalidRule(this, context);\r\n },\r\n};\r\n"],"mappings":";;;;;AAIA,MAAa,oBAAgC;CAC3C,MAAM;CACN,qBAAqB;CACrB,MAAM,SAAS,OAAO,SAAS;EAC7B,IAAI,iBAAiB,UAAU,OAAO;GACpC,KAAK,QAAQ,eAAe,QAAQ,KAAK,QAAQ,QAAQ,OAAO;GAChE,OAAO,YAAY,MAAM,OAAO;EAClC;EAEA,OAAO;CACT;AACF;AAEA,MAAa,qBAAsE;CACjF,MAAM;CACN,qBAAqB;CACrB,MAAM,SAAS,OAAO,SAAS;EAC7B,IAAI,EAAE,UAAU,KAAK,QAAQ;EAC7B,IAAI,OAAO,UAAU,UACnB,QAAQ,qBAAqB,KAAK;EAGpC,KAAK,QAAQ,eAAe,QAAQ,MAAM;EAE1C,IAAI,CAAC,MAAM,QAAQ,KAAK,GAAG,OAAO,YAAY,MAAM,OAAO;EAE3D,IAAI,MAAM,OAAO,SAAS,gBAAgB,KAAK,GAAG,OAAO;EAEzD,OAAO,YAAY,MAAM,OAAO;CAClC;AACF"}
1
+ {"version":3,"file":"database-model-rule.mjs","names":[],"sources":["../../../../../../../../cascade/src/validation/rules/database-model-rule.ts"],"sourcesContent":["import { invalidRule, VALID_RULE, type SchemaRule } from \"@warlock.js/seal\";\r\nimport { type ChildModel, Model } from \"./../../model/model\";\r\nimport { getModelFromRegistry } from \"./../../model/register-model\";\r\n\r\nexport const databaseModelRule: SchemaRule = {\r\n name: \"databaseModel\",\r\n defaultErrorMessage: \"The :input must be a valid :model model\",\r\n async validate(value, context) {\r\n if (value instanceof Model === false) {\r\n this.context.attributesList.model = this.context.options.model?.name;\r\n return invalidRule(this, context);\r\n }\r\n\r\n return VALID_RULE;\r\n },\r\n};\r\n\r\nexport const databaseModelsRule: SchemaRule<{ model: ChildModel<any> | string }> = {\r\n name: \"databaseModels\",\r\n defaultErrorMessage: \"The :input must be a list of valid :model\",\r\n async validate(value, context) {\r\n let { model } = this.context.options;\r\n if (typeof model === \"string\") {\r\n model = getModelFromRegistry(model)!;\r\n }\r\n\r\n this.context.attributesList.model = model.name;\r\n\r\n if (!Array.isArray(value)) return invalidRule(this, context);\r\n\r\n if (value.every((item) => item instanceof Model)) return VALID_RULE;\r\n\r\n return invalidRule(this, context);\r\n },\r\n};\r\n"],"mappings":";;;;;AAIA,MAAa,oBAAgC;CAC3C,MAAM;CACN,qBAAqB;CACrB,MAAM,SAAS,OAAO,SAAS;EAC7B,IAAI,iBAAiB,UAAU,OAAO;GACpC,KAAK,QAAQ,eAAe,QAAQ,KAAK,QAAQ,QAAQ,OAAO;GAChE,OAAO,YAAY,MAAM,OAAO;EAClC;EAEA,OAAO;CACT;AACF;AAEA,MAAa,qBAAsE;CACjF,MAAM;CACN,qBAAqB;CACrB,MAAM,SAAS,OAAO,SAAS;EAC7B,IAAI,EAAE,UAAU,KAAK,QAAQ;EAC7B,IAAI,OAAO,UAAU,UACnB,QAAQ,qBAAqB,KAAK;EAGpC,KAAK,QAAQ,eAAe,QAAQ,MAAM;EAE1C,IAAI,CAAC,MAAM,QAAQ,KAAK,GAAG,OAAO,YAAY,MAAM,OAAO;EAE3D,IAAI,MAAM,OAAO,SAAS,gBAAgB,KAAK,GAAG,OAAO;EAEzD,OAAO,YAAY,MAAM,OAAO;CAClC;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"exists-rule.mjs","names":[],"sources":["../../../../../../../../cascade/src/validation/rules/exists-rule.ts"],"sourcesContent":["import { invalidRule, VALID_RULE, type SchemaRule } from \"@warlock.js/seal\";\nimport { resolveModelClass } from \"../../model/register-model\";\nimport type { ExistsRuleOptions } from \"../types\";\n\n/**\n * Validates that a record exists in the database for the given column/value.\n *\n * @example\n * v.string().exists(Organization, { column: \"id\" });\n */\nexport const existsRule: SchemaRule<ExistsRuleOptions> = {\n name: \"exists\",\n defaultErrorMessage: \"The :input must exist\",\n async validate(value: any, context) {\n const { Model, query, column = context.key } = this.context.options;\n\n const ResolvedModelClass = resolveModelClass(Model);\n\n const dbQuery = ResolvedModelClass.query();\n\n dbQuery.where(column, value);\n\n if (query) {\n await query({\n query: dbQuery,\n value,\n allValues: context.allValues,\n });\n }\n\n const document = await dbQuery.first();\n\n return document ? VALID_RULE : invalidRule(this, context);\n },\n};\n"],"mappings":";;;;;;;;;;AAUA,MAAa,aAA4C;CACvD,MAAM;CACN,qBAAqB;CACrB,MAAM,SAAS,OAAY,SAAS;EAClC,MAAM,EAAE,OAAO,OAAO,SAAS,QAAQ,QAAQ,KAAK,QAAQ;EAI5D,MAAM,UAFqB,kBAAkB,KAEZ,CAAC,CAAC,MAAM;EAEzC,QAAQ,MAAM,QAAQ,KAAK;EAE3B,IAAI,OACF,MAAM,MAAM;GACV,OAAO;GACP;GACA,WAAW,QAAQ;EACrB,CAAC;EAKH,OAAO,MAFgB,QAAQ,MAAM,IAEnB,aAAa,YAAY,MAAM,OAAO;CAC1D;AACF"}
1
+ {"version":3,"file":"exists-rule.mjs","names":[],"sources":["../../../../../../../../cascade/src/validation/rules/exists-rule.ts"],"sourcesContent":["import { invalidRule, VALID_RULE, type SchemaRule } from \"@warlock.js/seal\";\nimport { resolveModelClass } from \"../../model/register-model\";\nimport type { ExistsRuleOptions } from \"../types\";\n\n/**\n * Validates that a record exists in the database for the given column/value.\n *\n * @example\n * v.string().exists(Organization, { column: \"id\" });\n */\nexport const existsRule: SchemaRule<ExistsRuleOptions> = {\n name: \"exists\",\n defaultErrorMessage: \"The :input must exist\",\n async validate(value: any, context) {\n const { Model, query, column = context.key } = this.context.options;\n\n const ResolvedModelClass = resolveModelClass(Model);\n\n const dbQuery = ResolvedModelClass.query();\n\n dbQuery.where(column, value);\n\n if (query) {\n await query({\n query: dbQuery,\n value,\n allValues: context.allValues,\n });\n }\n\n const document = await dbQuery.first();\n\n return document ? VALID_RULE : invalidRule(this, context);\n },\n};\n"],"mappings":";;;;;;;;;;AAUA,MAAa,aAA4C;CACvD,MAAM;CACN,qBAAqB;CACrB,MAAM,SAAS,OAAY,SAAS;EAClC,MAAM,EAAE,OAAO,OAAO,SAAS,QAAQ,QAAQ,KAAK,QAAQ;EAI5D,MAAM,UAFqB,kBAAkB,KAEZ,EAAE,MAAM;EAEzC,QAAQ,MAAM,QAAQ,KAAK;EAE3B,IAAI,OACF,MAAM,MAAM;GACV,OAAO;GACP;GACA,WAAW,QAAQ;EACrB,CAAC;EAKH,OAAO,MAFgB,QAAQ,MAAM,IAEnB,aAAa,YAAY,MAAM,OAAO;CAC1D;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"unique-rule.mjs","names":[],"sources":["../../../../../../../../cascade/src/validation/rules/unique-rule.ts"],"sourcesContent":["import { get } from \"@mongez/reinforcements\";\nimport { invalidRule, VALID_RULE, type SchemaRule } from \"@warlock.js/seal\";\nimport { resolveModelClass } from \"../../model/register-model\";\nimport type { UniqueRuleOptions } from \"../types\";\n\n/**\n * Validates that a value is unique against a database column.\n *\n * Supports three exclusion modes:\n * - `except`: read a sibling input's value and exclude rows where that\n * column equals the sibling's value\n * - `exceptColumnName` + `exceptValue`: exclude rows where the named\n * column equals the supplied value (used by request-aware wrappers)\n * - Custom `query` callback for anything more involved\n *\n * @example\n * v.email().unique(\"User\");\n * v.string().unique(\"User\", { except: \"id\" });\n */\nexport const uniqueRule: SchemaRule<UniqueRuleOptions> = {\n name: \"unique\",\n defaultErrorMessage: \"The :input must be unique\",\n async validate(value: any, context) {\n const {\n Model,\n except,\n column = context.key,\n exceptColumnName,\n exceptValue,\n query,\n } = this.context.options;\n\n const ResolvedModelClass = resolveModelClass(Model);\n\n const dbQuery = ResolvedModelClass.query();\n\n dbQuery.where(column, value);\n\n if (except) {\n const exceptVal = get(context.allValues, except);\n\n if (exceptVal !== undefined) {\n dbQuery.where(except, \"!=\", exceptVal);\n }\n }\n\n if (exceptColumnName !== undefined) {\n dbQuery.where(exceptColumnName, \"!=\", exceptValue);\n }\n\n if (query) {\n await query({\n query: dbQuery,\n value,\n allValues: context.allValues,\n });\n }\n\n const document = await dbQuery.first();\n\n return document ? invalidRule(this, context) : VALID_RULE;\n },\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,MAAa,aAA4C;CACvD,MAAM;CACN,qBAAqB;CACrB,MAAM,SAAS,OAAY,SAAS;EAClC,MAAM,EACJ,OACA,QACA,SAAS,QAAQ,KACjB,kBACA,aACA,UACE,KAAK,QAAQ;EAIjB,MAAM,UAFqB,kBAAkB,KAEZ,CAAC,CAAC,MAAM;EAEzC,QAAQ,MAAM,QAAQ,KAAK;EAE3B,IAAI,QAAQ;GACV,MAAM,YAAY,IAAI,QAAQ,WAAW,MAAM;GAE/C,IAAI,cAAc,QAChB,QAAQ,MAAM,QAAQ,MAAM,SAAS;EAEzC;EAEA,IAAI,qBAAqB,QACvB,QAAQ,MAAM,kBAAkB,MAAM,WAAW;EAGnD,IAAI,OACF,MAAM,MAAM;GACV,OAAO;GACP;GACA,WAAW,QAAQ;EACrB,CAAC;EAKH,OAAO,MAFgB,QAAQ,MAAM,IAEnB,YAAY,MAAM,OAAO,IAAI;CACjD;AACF"}
1
+ {"version":3,"file":"unique-rule.mjs","names":[],"sources":["../../../../../../../../cascade/src/validation/rules/unique-rule.ts"],"sourcesContent":["import { get } from \"@mongez/reinforcements\";\nimport { invalidRule, VALID_RULE, type SchemaRule } from \"@warlock.js/seal\";\nimport { resolveModelClass } from \"../../model/register-model\";\nimport type { UniqueRuleOptions } from \"../types\";\n\n/**\n * Validates that a value is unique against a database column.\n *\n * Supports three exclusion modes:\n * - `except`: read a sibling input's value and exclude rows where that\n * column equals the sibling's value\n * - `exceptColumnName` + `exceptValue`: exclude rows where the named\n * column equals the supplied value (used by request-aware wrappers)\n * - Custom `query` callback for anything more involved\n *\n * @example\n * v.email().unique(\"User\");\n * v.string().unique(\"User\", { except: \"id\" });\n */\nexport const uniqueRule: SchemaRule<UniqueRuleOptions> = {\n name: \"unique\",\n defaultErrorMessage: \"The :input must be unique\",\n async validate(value: any, context) {\n const {\n Model,\n except,\n column = context.key,\n exceptColumnName,\n exceptValue,\n query,\n } = this.context.options;\n\n const ResolvedModelClass = resolveModelClass(Model);\n\n const dbQuery = ResolvedModelClass.query();\n\n dbQuery.where(column, value);\n\n if (except) {\n const exceptVal = get(context.allValues, except);\n\n if (exceptVal !== undefined) {\n dbQuery.where(except, \"!=\", exceptVal);\n }\n }\n\n if (exceptColumnName !== undefined) {\n dbQuery.where(exceptColumnName, \"!=\", exceptValue);\n }\n\n if (query) {\n await query({\n query: dbQuery,\n value,\n allValues: context.allValues,\n });\n }\n\n const document = await dbQuery.first();\n\n return document ? invalidRule(this, context) : VALID_RULE;\n },\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,MAAa,aAA4C;CACvD,MAAM;CACN,qBAAqB;CACrB,MAAM,SAAS,OAAY,SAAS;EAClC,MAAM,EACJ,OACA,QACA,SAAS,QAAQ,KACjB,kBACA,aACA,UACE,KAAK,QAAQ;EAIjB,MAAM,UAFqB,kBAAkB,KAEZ,EAAE,MAAM;EAEzC,QAAQ,MAAM,QAAQ,KAAK;EAE3B,IAAI,QAAQ;GACV,MAAM,YAAY,IAAI,QAAQ,WAAW,MAAM;GAE/C,IAAI,cAAc,QAChB,QAAQ,MAAM,QAAQ,MAAM,SAAS;EAEzC;EAEA,IAAI,qBAAqB,QACvB,QAAQ,MAAM,kBAAkB,MAAM,WAAW;EAGnD,IAAI,OACF,MAAM,MAAM;GACV,OAAO;GACP;GACA,WAAW,QAAQ;EACrB,CAAC;EAKH,OAAO,MAFgB,QAAQ,MAAM,IAEnB,YAAY,MAAM,OAAO,IAAI;CACjD;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"database-writer.d.mts","names":[],"sources":["../../../../../../../cascade/src/writer/database-writer.ts"],"mappings":";;;;;;AAuDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAAa,cAAA,YAA0B,cAAA;EA+UN;EAAA,iBA7Ud,KAAA;EA6aT;EAAA,iBA1aS,IAAA;EAydT;EAAA,iBAtdS,UAAA;EAmeQ;EAAA,iBAheR,MAAA;;mBAGA,KAAA;;mBAGA,UAAA;;mBAGA,MAAA;;mBAGA,UAAA;;;;;;;;;;;;;cAcE,KAAA,EAAO,KAAA;;;;;;;;EAkBb,IAAA,CAAK,OAAA,GAAS,aAAA,GAAqB,OAAA,CAAQ,YAAA;;;;;;;;;;;UAsE1C,eAAA;;;;;;;;UA8FA,aAAA;;;;;;;;UAqDA,aAAA;;;;;;;;;;;;UAsDN,qBAAA;;;;;;EASK,cAAA,IAAkB,OAAA;;;;;;;;;;;;;;;;;;;;;;UA6CvB,qBAAA;;;;;;;;;;;;UAmDA,gBAAA;;;;;;;;;;;;UAyBA,kBAAA;;;;;;;;;UAsBA,SAAA;;;;;;;;;;UAaM,WAAA;AAAA"}
1
+ {"version":3,"file":"database-writer.d.mts","names":[],"sources":["../../../../../../../cascade/src/writer/database-writer.ts"],"mappings":";;;;;;AAuDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAAa,cAAA,YAA0B,cAAA;EA+UN;EAAA,iBA7Ud,KAAA;EA6aT;EAAA,iBA1aS,IAAA;EAydT;EAAA,iBAtdS,UAAA;EAmeQ;EAAA,iBAheR,MAAA;;mBAGA,KAAA;;mBAGA,UAAA;;mBAGA,MAAA;;mBAGA,UAAA;;;;;;;;;;;;;cAcE,KAAA,EAAO,KAAA;;;;;;;;EAkBb,IAAA,CAAK,OAAA,GAAS,aAAA,GAAqB,OAAA,CAAQ,YAAA;;;;;;;;;;;UAsE1C,eAAA;;;;;;;;UA8FA,aAAA;;;;;;;;UAqDA,aAAA;;;;;;;;;;;;UAsDN,qBAAA;;;;;;EASK,cAAA,CAAA,GAAkB,OAAA;;;;;;;;;;;;;;;;;;;;;;UA6CvB,qBAAA;;;;;;;;;;;;UAmDA,gBAAA;;;;;;;;;;;;UAyBA,kBAAA;;;;;;;;;UAsBA,SAAA;;;;;;;;;;UAaM,WAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"database-writer.mjs","names":[],"sources":["../../../../../../../cascade/src/writer/database-writer.ts"],"sourcesContent":["import events from \"@mongez/events\";\r\nimport { when } from \"@mongez/reinforcements\";\r\nimport { getSealConfig, v, type ObjectValidator } from \"@warlock.js/seal\";\r\nimport type {\r\n DriverContract,\r\n InsertResult,\r\n UpdateOperations,\r\n UpdateResult,\r\n} from \"../contracts/database-driver.contract\";\r\nimport type {\r\n WriterContract,\r\n WriterOptions,\r\n WriterResult,\r\n} from \"../contracts/database-writer.contract\";\r\nimport { mergeDriverFields } from \"../model/methods/accessor-methods\";\r\nimport type { ChildModel, Model } from \"../model/model\";\r\nimport { getModelUpdatedEvent } from \"../sync/model-events\";\r\nimport type { StrictMode } from \"../types\";\r\nimport { DatabaseWriterValidationError } from \"../validation\";\r\nimport type { DataSource } from \"./../data-source/data-source\";\r\n\r\n/**\r\n * Database writer service that orchestrates model persistence.\r\n *\r\n * Handles the complete save pipeline:\r\n * 1. Check for changes (skip if no changes and not new)\r\n * 2. Emit `saving` event (for data enrichment)\r\n * 3. Emit `validating` event\r\n * 4. Validate and cast data via @warlock.js/seal schema\r\n * 5. Emit `validated` event\r\n * 6. Generate ID (for new NoSQL records)\r\n * 7. Emit `creating`/`updating` events\r\n * 8. Execute insert or update via driver\r\n * 9. Merge returned data into model\r\n * 10. Reset dirty tracker and update `isNew` flag\r\n * 11. Emit `saved` and `created`/`updated` events\r\n *\r\n * @example\r\n * ```typescript\r\n * const user = new User({ name: \"Alice\", email: \"alice@example.com\" });\r\n * const writer = new DatabaseWriter(user);\r\n * await writer.save();\r\n *\r\n * console.log(user.get(\"id\")); // 1 (auto-generated)\r\n * console.log(user.get(\"_id\")); // ObjectId(\"...\")\r\n *\r\n * // Update existing record\r\n * user.set(\"name\", \"Alice Smith\");\r\n * await writer.save();\r\n * // Only updates the \"name\" field (partial update)\r\n *\r\n * // Silent save (no events)\r\n * await writer.save({ skipEvents: true });\r\n * ```\r\n */\r\nexport class DatabaseWriter implements WriterContract {\r\n /** The model instance being persisted */\r\n private readonly model: Model;\r\n\r\n /** Model constructor reference */\r\n private readonly ctor: ChildModel<Model>;\r\n\r\n /** Data source containing driver and ID generator */\r\n private readonly dataSource: DataSource;\r\n\r\n /** Database driver for executing queries */\r\n private readonly driver: DriverContract;\r\n\r\n /** Table/collection name */\r\n private readonly table: string;\r\n\r\n /** Primary key field name */\r\n private readonly primaryKey: string;\r\n\r\n /** Validation schema (if defined) */\r\n private readonly schema?: ObjectValidator;\r\n\r\n /** Strict mode configuration */\r\n private readonly strictMode: StrictMode;\r\n\r\n /**\r\n * Create a new writer instance for a model.\r\n *\r\n * @param model - The model instance to persist\r\n *\r\n * @example\r\n * ```typescript\r\n * const user = new User({ name: \"Alice\" });\r\n * const writer = new DatabaseWriter(user);\r\n * await writer.save();\r\n * ```\r\n */\r\n public constructor(model: Model) {\r\n this.model = model;\r\n this.ctor = model.constructor as ChildModel<Model>;\r\n this.dataSource = this.ctor.getDataSource();\r\n this.driver = this.dataSource.driver;\r\n this.table = this.ctor.table;\r\n this.primaryKey = this.ctor.primaryKey;\r\n this.schema = this.ctor.schema;\r\n this.strictMode = this.ctor.strictMode;\r\n }\r\n\r\n /**\r\n * Save the model instance to the database.\r\n *\r\n * @param options - Save options\r\n * @returns Result with success status, document, and metadata\r\n * @throws {ValidationError} If validation fails\r\n */\r\n public async save(options: WriterOptions = {}): Promise<WriterResult> {\r\n const isInsert = this.model.isNew;\r\n\r\n // 1. Check if model has changes (skip if no changes and not new)\r\n if (!isInsert && !this.model.hasChanges()) {\r\n return {\r\n success: true,\r\n document: this.model.data,\r\n isNew: false,\r\n modifiedCount: 0,\r\n };\r\n }\r\n\r\n // 2. Emit saving event (before validation for data enrichment)\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"saving\", {\r\n isInsert,\r\n options,\r\n mode: isInsert ? \"insert\" : \"update\",\r\n });\r\n }\r\n\r\n // 3. Validate and cast data\r\n await this.validateAndCast(isInsert, options);\r\n\r\n // 4. Execute insert or update\r\n let result: InsertResult | UpdateResult;\r\n\r\n if (isInsert) {\r\n result = await this.performInsert(options);\r\n } else {\r\n result = await this.performUpdate(options);\r\n }\r\n\r\n // 5. Reset dirty tracker and update isNew flag\r\n const changedFields = isInsert ? [] : this.model.getDirtyColumns();\r\n this.model.dirtyTracker.reset();\r\n this.model.isNew = false;\r\n\r\n // 6. Emit post-save events\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"saved\");\r\n await this.model.emitEvent(isInsert ? \"created\" : \"updated\");\r\n }\r\n\r\n // 7. Trigger sync operations (fire-and-forget, non-blocking)\r\n if (!options.skipSync && !isInsert) {\r\n void this.triggerSync(changedFields);\r\n }\r\n\r\n return {\r\n success: true,\r\n document: this.model.data,\r\n isNew: isInsert,\r\n modifiedCount: isInsert\r\n ? undefined\r\n : (result as UpdateResult).modifiedCount,\r\n };\r\n }\r\n\r\n /**\r\n * Validate and cast model data using the schema.\r\n *\r\n * Updates the model's data in-place with validated/casted values.\r\n *\r\n * @param isInsert - Whether this is an insert operation\r\n * @param options - Save options\r\n * @throws {ValidationError} If validation fails\r\n * @private\r\n */\r\n private async validateAndCast(\r\n isInsert: boolean,\r\n options: WriterOptions,\r\n ): Promise<void> {\r\n // Emit validating event\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"validating\", {\r\n isInsert,\r\n options,\r\n mode: isInsert ? \"insert\" : \"update\",\r\n });\r\n }\r\n\r\n // Skip validation if requested or no schema defined\r\n if (options.skipValidation || !this.schema) {\r\n return;\r\n }\r\n\r\n // Whitelist the framework-managed system columns so a model carrying them\r\n // (id / _id / timestamps / soft-delete deletedAt) validates cleanly instead\r\n // of being stripped (strictMode \"strip\") or rejected (strictMode \"fail\").\r\n // `when(...)` adds each timestamp/soft-delete column only when configured\r\n // (truthy) — the lazy factory means a disabled column (`false`) never even\r\n // builds a bogus schema key.\r\n //\r\n // The whitelist must apply to BOTH insert and update: on insert a caller may\r\n // supply a backdated createdAt (e.g. data migrations / imports), and without\r\n // the whitelist strictMode \"strip\" silently drops it before it reaches the\r\n // writer, while \"fail\" rejects the whole insert.\r\n const systemColumns = {\r\n id: v.scalar().optional(),\r\n _id: v.any().optional(),\r\n ...when(this.ctor.createdAtColumn, () => ({\r\n [this.ctor.createdAtColumn as string]: v.date().optional(),\r\n })),\r\n ...when(this.ctor.updatedAtColumn, () => ({\r\n [this.ctor.updatedAtColumn as string]: v.date().optional(),\r\n })),\r\n ...when(this.ctor.deletedAtColumn, () => ({\r\n [this.ctor.deletedAtColumn as string]: v.date().optional(),\r\n })),\r\n };\r\n\r\n // Clone full schema for insert, partial (dirty keys only) for updates.\r\n const validationSchema = isInsert\r\n ? this.schema.clone().extend(systemColumns)\r\n : this.schema.clone(Object.keys(this.model.data)).extend(systemColumns);\r\n\r\n // Apply strict mode\r\n if (this.strictMode === \"strip\") {\r\n validationSchema.stripUnknown();\r\n } else if (this.strictMode === \"fail\") {\r\n validationSchema.allowUnknown(false);\r\n } else if (this.strictMode === \"allow\") {\r\n validationSchema.allowUnknown(true);\r\n }\r\n\r\n // Run validation\r\n const result = await v.validate(validationSchema, this.model.data, {\r\n context: {\r\n model: this.model,\r\n },\r\n ...getSealConfig(),\r\n });\r\n\r\n if (!result.isValid) {\r\n console.trace(result.errors);\r\n\r\n const error = new DatabaseWriterValidationError(\r\n `[${this.model.constructor.name} Model] ${isInsert ? \"Insert\" : \"Update\"} Validation failed`,\r\n result.errors,\r\n );\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"validated\", { result, error });\r\n }\r\n throw error;\r\n }\r\n\r\n // Update model data with validated/casted data\r\n this.model.replaceData(result.data);\r\n\r\n // Emit validated event\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"validated\", { result });\r\n }\r\n }\r\n\r\n /**\r\n * Perform an insert operation.\r\n *\r\n * @param options - Save options\r\n * @returns Insert result\r\n * @private\r\n */\r\n private async performInsert(options: WriterOptions): Promise<InsertResult> {\r\n // Generate ID if needed (NoSQL only)\r\n await this.generateNextId();\r\n\r\n // Get data to insert (already validated and casted)\r\n const dataToInsert = this.model.data;\r\n\r\n // Add createdAt and updatedAt to the data (using resolved column names)\r\n // The column names are already resolved through the hierarchy:\r\n // Model static property > Database config > Driver defaults > undefined\r\n //\r\n // Only stamp createdAt when the caller did NOT supply one, so a backdated\r\n // value (e.g. data migrations / imports) is honored instead of overwritten.\r\n // This mirrors the upsert path's guard. updatedAt is always stamped to\r\n // reflect the moment the record is persisted.\r\n const createdAtColumn = this.ctor.createdAtColumn;\r\n\r\n if (createdAtColumn && dataToInsert[createdAtColumn] == null) {\r\n dataToInsert[createdAtColumn] = new Date();\r\n }\r\n\r\n const updatedAtColumn = this.ctor.updatedAtColumn;\r\n if (updatedAtColumn) {\r\n dataToInsert[updatedAtColumn] = new Date();\r\n }\r\n\r\n // Emit creating event\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"creating\");\r\n }\r\n\r\n // INSERT: use full validated data\r\n const result = await this.driver.insert(this.table, dataToInsert);\r\n\r\n // Merge returned data (e.g., generated _id, timestamps)\r\n // Note: We use merge here because the result might not include all fields\r\n // (e.g., our generated 'id' field), and we don't want to lose them\r\n mergeDriverFields(this.model, result.document as Record<string, unknown>);\r\n\r\n // Reset dirty tracker immediately after merge to prevent\r\n // database-generated fields (like _id) from being marked as dirty\r\n this.model.dirtyTracker.reset();\r\n\r\n return result;\r\n }\r\n\r\n /**\r\n * Perform an update operation.\r\n *\r\n * @param options - Save options\r\n * @returns Update result\r\n * @private\r\n */\r\n private async performUpdate(options: WriterOptions): Promise<UpdateResult> {\r\n // Emit updating event\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"updating\");\r\n }\r\n\r\n // Update the updatedAt timestamp (using resolved column name)\r\n const updatedAtColumn = this.ctor.updatedAtColumn;\r\n if (updatedAtColumn) {\r\n this.model.set(updatedAtColumn, new Date());\r\n }\r\n\r\n if (options.replace) {\r\n const document = await this.driver.replace(\r\n this.table,\r\n this.buildPrimaryKeyFilter(),\r\n this.model.data,\r\n );\r\n\r\n if (document) {\r\n this.model.replaceData(document as Record<string, unknown>);\r\n }\r\n\r\n return { modifiedCount: document ? 1 : 0 };\r\n }\r\n\r\n // Build operations from dirty tracker\r\n const operations = this.buildUpdateOperations();\r\n\r\n // Nothing left to write once identity columns are excluded — don't hand the\r\n // driver an empty update document (MongoDB rejects one).\r\n if (Object.keys(operations).length === 0) {\r\n return { modifiedCount: 0 };\r\n }\r\n\r\n // Execute update with operations\r\n return await this.driver.update(\r\n this.table,\r\n this.buildPrimaryKeyFilter(),\r\n operations,\r\n );\r\n }\r\n\r\n /**\r\n * Build the filter that pins a write to the row this model was loaded from.\r\n *\r\n * It reads `model.trustedPrimaryKey` — the value captured when the instance\r\n * became persisted — NOT the current value in `model.data`. The current value\r\n * is reachable by mass assignment (`model.merge(req.body)`), so deriving the\r\n * filter from it let a request body redirect the UPDATE to another document.\r\n *\r\n * @returns Filter matching the originally loaded record\r\n * @private\r\n */\r\n private buildPrimaryKeyFilter(): Record<string, unknown> {\r\n return { [this.primaryKey]: this.model.trustedPrimaryKey };\r\n }\r\n\r\n /**\r\n * Generate ID for the model if auto-generation is enabled.\r\n *\r\n * @private\r\n */\r\n public async generateNextId(): Promise<void> {\r\n if (!this.ctor.autoGenerateId || this.model.get(\"id\")) {\r\n return;\r\n }\r\n\r\n const idGenerator = this.dataSource.idGenerator;\r\n if (!idGenerator) {\r\n return;\r\n }\r\n\r\n // Resolve ID generation options from model configuration\r\n const initialId = this.resolveInitialId();\r\n\r\n const incrementIdBy = this.resolveIncrementBy();\r\n\r\n const id = await idGenerator.generateNextId({\r\n table: this.table,\r\n initialId,\r\n incrementIdBy,\r\n });\r\n\r\n this.model.set(\"id\", id);\r\n }\r\n\r\n /**\r\n * Build update operations from the model's dirty tracker.\r\n *\r\n * Handles both modified fields ($set) and removed fields ($unset).\r\n *\r\n * @returns Update operations for the driver\r\n * @private\r\n *\r\n * @example\r\n * ```typescript\r\n * // Model with changes\r\n * user.set(\"name\", \"Alice\");\r\n * user.unset(\"tempField\");\r\n *\r\n * const operations = this.buildUpdateOperations();\r\n * // {\r\n * // $set: { name: \"Alice\" },\r\n * // $unset: { tempField: 1 }\r\n * // }\r\n * ```\r\n */\r\n private buildUpdateOperations(): UpdateOperations {\r\n const operations: UpdateOperations = {};\r\n\r\n // Identity columns are never written by an update. The filter pins the row\r\n // by its captured primary key, so a dirty `id`/`_id` can only come from\r\n // mass assignment or an explicit set() — either way, rewriting the key of\r\n // an existing row corrupts identity (and `_id` is immutable in MongoDB).\r\n // Changing a primary key is a deliberate operation: use the atomic/raw APIs.\r\n const identityColumns = new Set([this.primaryKey, \"id\", \"_id\"]);\r\n\r\n // Get dirty columns (modified fields)\r\n const dirtyColumns = this.model\r\n .getDirtyColumns()\r\n .filter(column => !identityColumns.has(column));\r\n\r\n if (dirtyColumns.length > 0) {\r\n operations.$set = {};\r\n for (const column of dirtyColumns) {\r\n const value = this.model.get(column);\r\n if (value === undefined) continue;\r\n\r\n operations.$set[column] = this.model.get(column);\r\n }\r\n }\r\n\r\n // Get removed columns\r\n const removedColumns = this.model\r\n .getRemovedColumns()\r\n .filter(column => !identityColumns.has(column));\r\n\r\n if (removedColumns.length > 0) {\r\n operations.$unset = {};\r\n for (const column of removedColumns) {\r\n operations.$unset[column] = 1;\r\n }\r\n }\r\n\r\n return operations;\r\n }\r\n\r\n /**\r\n * Resolve the initial ID from model configuration.\r\n *\r\n * Priority:\r\n * 1. Model.initialId (explicit value)\r\n * 2. Model.randomInitialId (random or function)\r\n * 3. Default: 1\r\n *\r\n * @returns The initial ID value\r\n * @private\r\n */\r\n private resolveInitialId(): number {\r\n if (this.ctor.initialId) {\r\n return this.ctor.initialId;\r\n }\r\n\r\n if (this.ctor.randomInitialId) {\r\n return typeof this.ctor.randomInitialId === \"function\"\r\n ? this.ctor.randomInitialId()\r\n : this.randomInt(10000, 499999);\r\n }\r\n\r\n return 1; // Default initial ID\r\n }\r\n\r\n /**\r\n * Resolve the increment value from model configuration.\r\n *\r\n * Priority:\r\n * 1. Model.incrementIdBy (explicit value)\r\n * 2. Model.randomIncrement (random or function)\r\n * 3. Default: 1\r\n *\r\n * @returns The increment value\r\n * @private\r\n */\r\n private resolveIncrementBy(): number {\r\n if (this.ctor.incrementIdBy) {\r\n return this.ctor.incrementIdBy;\r\n }\r\n\r\n if (this.ctor.randomIncrement) {\r\n return typeof this.ctor.randomIncrement === \"function\"\r\n ? this.ctor.randomIncrement()\r\n : this.randomInt(1, 10);\r\n }\r\n\r\n return 1; // Default increment\r\n }\r\n\r\n /**\r\n * Generate a random integer between min and max (inclusive).\r\n *\r\n * @param min - Minimum value\r\n * @param max - Maximum value\r\n * @returns Random integer\r\n * @private\r\n */\r\n private randomInt(min: number, max: number): number {\r\n return Math.floor(Math.random() * (max - min + 1)) + min;\r\n }\r\n\r\n /**\r\n * Trigger sync operations after successful save.\r\n *\r\n * Emits a model.updated event that ModelSyncOperation listens to.\r\n * The sync is handled by registered sync operations, not directly here.\r\n *\r\n * @param changedFields - Fields that were changed (for filtering)\r\n * @private\r\n */\r\n private async triggerSync(changedFields: string[]): Promise<void> {\r\n // Emit model.updated event - ModelSyncOperation listens to these\r\n await events.triggerAll(\r\n getModelUpdatedEvent(this.ctor),\r\n this.model,\r\n changedFields,\r\n );\r\n }\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"database-writer.mjs","names":[],"sources":["../../../../../../../cascade/src/writer/database-writer.ts"],"sourcesContent":["import events from \"@mongez/events\";\r\nimport { when } from \"@mongez/reinforcements\";\r\nimport { getSealConfig, v, type ObjectValidator } from \"@warlock.js/seal\";\r\nimport type {\r\n DriverContract,\r\n InsertResult,\r\n UpdateOperations,\r\n UpdateResult,\r\n} from \"../contracts/database-driver.contract\";\r\nimport type {\r\n WriterContract,\r\n WriterOptions,\r\n WriterResult,\r\n} from \"../contracts/database-writer.contract\";\r\nimport { mergeDriverFields } from \"../model/methods/accessor-methods\";\r\nimport type { ChildModel, Model } from \"../model/model\";\r\nimport { getModelUpdatedEvent } from \"../sync/model-events\";\r\nimport type { StrictMode } from \"../types\";\r\nimport { DatabaseWriterValidationError } from \"../validation\";\r\nimport type { DataSource } from \"./../data-source/data-source\";\r\n\r\n/**\r\n * Database writer service that orchestrates model persistence.\r\n *\r\n * Handles the complete save pipeline:\r\n * 1. Check for changes (skip if no changes and not new)\r\n * 2. Emit `saving` event (for data enrichment)\r\n * 3. Emit `validating` event\r\n * 4. Validate and cast data via @warlock.js/seal schema\r\n * 5. Emit `validated` event\r\n * 6. Generate ID (for new NoSQL records)\r\n * 7. Emit `creating`/`updating` events\r\n * 8. Execute insert or update via driver\r\n * 9. Merge returned data into model\r\n * 10. Reset dirty tracker and update `isNew` flag\r\n * 11. Emit `saved` and `created`/`updated` events\r\n *\r\n * @example\r\n * ```typescript\r\n * const user = new User({ name: \"Alice\", email: \"alice@example.com\" });\r\n * const writer = new DatabaseWriter(user);\r\n * await writer.save();\r\n *\r\n * console.log(user.get(\"id\")); // 1 (auto-generated)\r\n * console.log(user.get(\"_id\")); // ObjectId(\"...\")\r\n *\r\n * // Update existing record\r\n * user.set(\"name\", \"Alice Smith\");\r\n * await writer.save();\r\n * // Only updates the \"name\" field (partial update)\r\n *\r\n * // Silent save (no events)\r\n * await writer.save({ skipEvents: true });\r\n * ```\r\n */\r\nexport class DatabaseWriter implements WriterContract {\r\n /** The model instance being persisted */\r\n private readonly model: Model;\r\n\r\n /** Model constructor reference */\r\n private readonly ctor: ChildModel<Model>;\r\n\r\n /** Data source containing driver and ID generator */\r\n private readonly dataSource: DataSource;\r\n\r\n /** Database driver for executing queries */\r\n private readonly driver: DriverContract;\r\n\r\n /** Table/collection name */\r\n private readonly table: string;\r\n\r\n /** Primary key field name */\r\n private readonly primaryKey: string;\r\n\r\n /** Validation schema (if defined) */\r\n private readonly schema?: ObjectValidator;\r\n\r\n /** Strict mode configuration */\r\n private readonly strictMode: StrictMode;\r\n\r\n /**\r\n * Create a new writer instance for a model.\r\n *\r\n * @param model - The model instance to persist\r\n *\r\n * @example\r\n * ```typescript\r\n * const user = new User({ name: \"Alice\" });\r\n * const writer = new DatabaseWriter(user);\r\n * await writer.save();\r\n * ```\r\n */\r\n public constructor(model: Model) {\r\n this.model = model;\r\n this.ctor = model.constructor as ChildModel<Model>;\r\n this.dataSource = this.ctor.getDataSource();\r\n this.driver = this.dataSource.driver;\r\n this.table = this.ctor.table;\r\n this.primaryKey = this.ctor.primaryKey;\r\n this.schema = this.ctor.schema;\r\n this.strictMode = this.ctor.strictMode;\r\n }\r\n\r\n /**\r\n * Save the model instance to the database.\r\n *\r\n * @param options - Save options\r\n * @returns Result with success status, document, and metadata\r\n * @throws {ValidationError} If validation fails\r\n */\r\n public async save(options: WriterOptions = {}): Promise<WriterResult> {\r\n const isInsert = this.model.isNew;\r\n\r\n // 1. Check if model has changes (skip if no changes and not new)\r\n if (!isInsert && !this.model.hasChanges()) {\r\n return {\r\n success: true,\r\n document: this.model.data,\r\n isNew: false,\r\n modifiedCount: 0,\r\n };\r\n }\r\n\r\n // 2. Emit saving event (before validation for data enrichment)\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"saving\", {\r\n isInsert,\r\n options,\r\n mode: isInsert ? \"insert\" : \"update\",\r\n });\r\n }\r\n\r\n // 3. Validate and cast data\r\n await this.validateAndCast(isInsert, options);\r\n\r\n // 4. Execute insert or update\r\n let result: InsertResult | UpdateResult;\r\n\r\n if (isInsert) {\r\n result = await this.performInsert(options);\r\n } else {\r\n result = await this.performUpdate(options);\r\n }\r\n\r\n // 5. Reset dirty tracker and update isNew flag\r\n const changedFields = isInsert ? [] : this.model.getDirtyColumns();\r\n this.model.dirtyTracker.reset();\r\n this.model.isNew = false;\r\n\r\n // 6. Emit post-save events\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"saved\");\r\n await this.model.emitEvent(isInsert ? \"created\" : \"updated\");\r\n }\r\n\r\n // 7. Trigger sync operations (fire-and-forget, non-blocking)\r\n if (!options.skipSync && !isInsert) {\r\n void this.triggerSync(changedFields);\r\n }\r\n\r\n return {\r\n success: true,\r\n document: this.model.data,\r\n isNew: isInsert,\r\n modifiedCount: isInsert\r\n ? undefined\r\n : (result as UpdateResult).modifiedCount,\r\n };\r\n }\r\n\r\n /**\r\n * Validate and cast model data using the schema.\r\n *\r\n * Updates the model's data in-place with validated/casted values.\r\n *\r\n * @param isInsert - Whether this is an insert operation\r\n * @param options - Save options\r\n * @throws {ValidationError} If validation fails\r\n * @private\r\n */\r\n private async validateAndCast(\r\n isInsert: boolean,\r\n options: WriterOptions,\r\n ): Promise<void> {\r\n // Emit validating event\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"validating\", {\r\n isInsert,\r\n options,\r\n mode: isInsert ? \"insert\" : \"update\",\r\n });\r\n }\r\n\r\n // Skip validation if requested or no schema defined\r\n if (options.skipValidation || !this.schema) {\r\n return;\r\n }\r\n\r\n // Whitelist the framework-managed system columns so a model carrying them\r\n // (id / _id / timestamps / soft-delete deletedAt) validates cleanly instead\r\n // of being stripped (strictMode \"strip\") or rejected (strictMode \"fail\").\r\n // `when(...)` adds each timestamp/soft-delete column only when configured\r\n // (truthy) — the lazy factory means a disabled column (`false`) never even\r\n // builds a bogus schema key.\r\n //\r\n // The whitelist must apply to BOTH insert and update: on insert a caller may\r\n // supply a backdated createdAt (e.g. data migrations / imports), and without\r\n // the whitelist strictMode \"strip\" silently drops it before it reaches the\r\n // writer, while \"fail\" rejects the whole insert.\r\n const systemColumns = {\r\n id: v.scalar().optional(),\r\n _id: v.any().optional(),\r\n ...when(this.ctor.createdAtColumn, () => ({\r\n [this.ctor.createdAtColumn as string]: v.date().optional(),\r\n })),\r\n ...when(this.ctor.updatedAtColumn, () => ({\r\n [this.ctor.updatedAtColumn as string]: v.date().optional(),\r\n })),\r\n ...when(this.ctor.deletedAtColumn, () => ({\r\n [this.ctor.deletedAtColumn as string]: v.date().optional(),\r\n })),\r\n };\r\n\r\n // Clone full schema for insert, partial (dirty keys only) for updates.\r\n const validationSchema = isInsert\r\n ? this.schema.clone().extend(systemColumns)\r\n : this.schema.clone(Object.keys(this.model.data)).extend(systemColumns);\r\n\r\n // Apply strict mode\r\n if (this.strictMode === \"strip\") {\r\n validationSchema.stripUnknown();\r\n } else if (this.strictMode === \"fail\") {\r\n validationSchema.allowUnknown(false);\r\n } else if (this.strictMode === \"allow\") {\r\n validationSchema.allowUnknown(true);\r\n }\r\n\r\n // Run validation\r\n const result = await v.validate(validationSchema, this.model.data, {\r\n context: {\r\n model: this.model,\r\n },\r\n ...getSealConfig(),\r\n });\r\n\r\n if (!result.isValid) {\r\n console.trace(result.errors);\r\n\r\n const error = new DatabaseWriterValidationError(\r\n `[${this.model.constructor.name} Model] ${isInsert ? \"Insert\" : \"Update\"} Validation failed`,\r\n result.errors,\r\n );\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"validated\", { result, error });\r\n }\r\n throw error;\r\n }\r\n\r\n // Update model data with validated/casted data\r\n this.model.replaceData(result.data);\r\n\r\n // Emit validated event\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"validated\", { result });\r\n }\r\n }\r\n\r\n /**\r\n * Perform an insert operation.\r\n *\r\n * @param options - Save options\r\n * @returns Insert result\r\n * @private\r\n */\r\n private async performInsert(options: WriterOptions): Promise<InsertResult> {\r\n // Generate ID if needed (NoSQL only)\r\n await this.generateNextId();\r\n\r\n // Get data to insert (already validated and casted)\r\n const dataToInsert = this.model.data;\r\n\r\n // Add createdAt and updatedAt to the data (using resolved column names)\r\n // The column names are already resolved through the hierarchy:\r\n // Model static property > Database config > Driver defaults > undefined\r\n //\r\n // Only stamp createdAt when the caller did NOT supply one, so a backdated\r\n // value (e.g. data migrations / imports) is honored instead of overwritten.\r\n // This mirrors the upsert path's guard. updatedAt is always stamped to\r\n // reflect the moment the record is persisted.\r\n const createdAtColumn = this.ctor.createdAtColumn;\r\n\r\n if (createdAtColumn && dataToInsert[createdAtColumn] == null) {\r\n dataToInsert[createdAtColumn] = new Date();\r\n }\r\n\r\n const updatedAtColumn = this.ctor.updatedAtColumn;\r\n if (updatedAtColumn) {\r\n dataToInsert[updatedAtColumn] = new Date();\r\n }\r\n\r\n // Emit creating event\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"creating\");\r\n }\r\n\r\n // INSERT: use full validated data\r\n const result = await this.driver.insert(this.table, dataToInsert);\r\n\r\n // Merge returned data (e.g., generated _id, timestamps)\r\n // Note: We use merge here because the result might not include all fields\r\n // (e.g., our generated 'id' field), and we don't want to lose them\r\n mergeDriverFields(this.model, result.document as Record<string, unknown>);\r\n\r\n // Reset dirty tracker immediately after merge to prevent\r\n // database-generated fields (like _id) from being marked as dirty\r\n this.model.dirtyTracker.reset();\r\n\r\n return result;\r\n }\r\n\r\n /**\r\n * Perform an update operation.\r\n *\r\n * @param options - Save options\r\n * @returns Update result\r\n * @private\r\n */\r\n private async performUpdate(options: WriterOptions): Promise<UpdateResult> {\r\n // Emit updating event\r\n if (!options.skipEvents) {\r\n await this.model.emitEvent(\"updating\");\r\n }\r\n\r\n // Update the updatedAt timestamp (using resolved column name)\r\n const updatedAtColumn = this.ctor.updatedAtColumn;\r\n if (updatedAtColumn) {\r\n this.model.set(updatedAtColumn, new Date());\r\n }\r\n\r\n if (options.replace) {\r\n const document = await this.driver.replace(\r\n this.table,\r\n this.buildPrimaryKeyFilter(),\r\n this.model.data,\r\n );\r\n\r\n if (document) {\r\n this.model.replaceData(document as Record<string, unknown>);\r\n }\r\n\r\n return { modifiedCount: document ? 1 : 0 };\r\n }\r\n\r\n // Build operations from dirty tracker\r\n const operations = this.buildUpdateOperations();\r\n\r\n // Nothing left to write once identity columns are excluded — don't hand the\r\n // driver an empty update document (MongoDB rejects one).\r\n if (Object.keys(operations).length === 0) {\r\n return { modifiedCount: 0 };\r\n }\r\n\r\n // Execute update with operations\r\n return await this.driver.update(\r\n this.table,\r\n this.buildPrimaryKeyFilter(),\r\n operations,\r\n );\r\n }\r\n\r\n /**\r\n * Build the filter that pins a write to the row this model was loaded from.\r\n *\r\n * It reads `model.trustedPrimaryKey` — the value captured when the instance\r\n * became persisted — NOT the current value in `model.data`. The current value\r\n * is reachable by mass assignment (`model.merge(req.body)`), so deriving the\r\n * filter from it let a request body redirect the UPDATE to another document.\r\n *\r\n * @returns Filter matching the originally loaded record\r\n * @private\r\n */\r\n private buildPrimaryKeyFilter(): Record<string, unknown> {\r\n return { [this.primaryKey]: this.model.trustedPrimaryKey };\r\n }\r\n\r\n /**\r\n * Generate ID for the model if auto-generation is enabled.\r\n *\r\n * @private\r\n */\r\n public async generateNextId(): Promise<void> {\r\n if (!this.ctor.autoGenerateId || this.model.get(\"id\")) {\r\n return;\r\n }\r\n\r\n const idGenerator = this.dataSource.idGenerator;\r\n if (!idGenerator) {\r\n return;\r\n }\r\n\r\n // Resolve ID generation options from model configuration\r\n const initialId = this.resolveInitialId();\r\n\r\n const incrementIdBy = this.resolveIncrementBy();\r\n\r\n const id = await idGenerator.generateNextId({\r\n table: this.table,\r\n initialId,\r\n incrementIdBy,\r\n });\r\n\r\n this.model.set(\"id\", id);\r\n }\r\n\r\n /**\r\n * Build update operations from the model's dirty tracker.\r\n *\r\n * Handles both modified fields ($set) and removed fields ($unset).\r\n *\r\n * @returns Update operations for the driver\r\n * @private\r\n *\r\n * @example\r\n * ```typescript\r\n * // Model with changes\r\n * user.set(\"name\", \"Alice\");\r\n * user.unset(\"tempField\");\r\n *\r\n * const operations = this.buildUpdateOperations();\r\n * // {\r\n * // $set: { name: \"Alice\" },\r\n * // $unset: { tempField: 1 }\r\n * // }\r\n * ```\r\n */\r\n private buildUpdateOperations(): UpdateOperations {\r\n const operations: UpdateOperations = {};\r\n\r\n // Identity columns are never written by an update. The filter pins the row\r\n // by its captured primary key, so a dirty `id`/`_id` can only come from\r\n // mass assignment or an explicit set() — either way, rewriting the key of\r\n // an existing row corrupts identity (and `_id` is immutable in MongoDB).\r\n // Changing a primary key is a deliberate operation: use the atomic/raw APIs.\r\n const identityColumns = new Set([this.primaryKey, \"id\", \"_id\"]);\r\n\r\n // Get dirty columns (modified fields)\r\n const dirtyColumns = this.model\r\n .getDirtyColumns()\r\n .filter(column => !identityColumns.has(column));\r\n\r\n if (dirtyColumns.length > 0) {\r\n operations.$set = {};\r\n for (const column of dirtyColumns) {\r\n const value = this.model.get(column);\r\n if (value === undefined) continue;\r\n\r\n operations.$set[column] = this.model.get(column);\r\n }\r\n }\r\n\r\n // Get removed columns\r\n const removedColumns = this.model\r\n .getRemovedColumns()\r\n .filter(column => !identityColumns.has(column));\r\n\r\n if (removedColumns.length > 0) {\r\n operations.$unset = {};\r\n for (const column of removedColumns) {\r\n operations.$unset[column] = 1;\r\n }\r\n }\r\n\r\n return operations;\r\n }\r\n\r\n /**\r\n * Resolve the initial ID from model configuration.\r\n *\r\n * Priority:\r\n * 1. Model.initialId (explicit value)\r\n * 2. Model.randomInitialId (random or function)\r\n * 3. Default: 1\r\n *\r\n * @returns The initial ID value\r\n * @private\r\n */\r\n private resolveInitialId(): number {\r\n if (this.ctor.initialId) {\r\n return this.ctor.initialId;\r\n }\r\n\r\n if (this.ctor.randomInitialId) {\r\n return typeof this.ctor.randomInitialId === \"function\"\r\n ? this.ctor.randomInitialId()\r\n : this.randomInt(10000, 499999);\r\n }\r\n\r\n return 1; // Default initial ID\r\n }\r\n\r\n /**\r\n * Resolve the increment value from model configuration.\r\n *\r\n * Priority:\r\n * 1. Model.incrementIdBy (explicit value)\r\n * 2. Model.randomIncrement (random or function)\r\n * 3. Default: 1\r\n *\r\n * @returns The increment value\r\n * @private\r\n */\r\n private resolveIncrementBy(): number {\r\n if (this.ctor.incrementIdBy) {\r\n return this.ctor.incrementIdBy;\r\n }\r\n\r\n if (this.ctor.randomIncrement) {\r\n return typeof this.ctor.randomIncrement === \"function\"\r\n ? this.ctor.randomIncrement()\r\n : this.randomInt(1, 10);\r\n }\r\n\r\n return 1; // Default increment\r\n }\r\n\r\n /**\r\n * Generate a random integer between min and max (inclusive).\r\n *\r\n * @param min - Minimum value\r\n * @param max - Maximum value\r\n * @returns Random integer\r\n * @private\r\n */\r\n private randomInt(min: number, max: number): number {\r\n return Math.floor(Math.random() * (max - min + 1)) + min;\r\n }\r\n\r\n /**\r\n * Trigger sync operations after successful save.\r\n *\r\n * Emits a model.updated event that ModelSyncOperation listens to.\r\n * The sync is handled by registered sync operations, not directly here.\r\n *\r\n * @param changedFields - Fields that were changed (for filtering)\r\n * @private\r\n */\r\n private async triggerSync(changedFields: string[]): Promise<void> {\r\n // Emit model.updated event - ModelSyncOperation listens to these\r\n await events.triggerAll(\r\n getModelUpdatedEvent(this.ctor),\r\n this.model,\r\n changedFields,\r\n );\r\n }\r\n}\r\n"],"mappings":"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"}
package/llms-full.txt CHANGED
@@ -698,6 +698,8 @@ Model-first TypeScript ORM for MongoDB and Postgres. Query straight off the mode
698
698
 
699
699
  > This skill is the cascade **map** — read it first, then load the specific skill for the task.
700
700
 
701
+ Server-only: `package.json` declares `"warlock": { "environment": "server" }`. `@warlock.js/web`'s build refuses app client code that value-imports this package; type-only imports are allowed, and server loaders/controllers/modules are unaffected.
702
+
701
703
  ## Install
702
704
 
703
705
  ```bash
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@warlock.js/cascade",
3
3
  "description": "ORM for managing databases",
4
+ "warlock": {
5
+ "environment": "server"
6
+ },
4
7
  "bin": {
5
8
  "cascade": "bin/cascade.js"
6
9
  },
@@ -10,9 +13,9 @@
10
13
  "@mongez/events": "^2.2.7",
11
14
  "@mongez/reinforcements": "^4.0.1",
12
15
  "@mongez/supportive-is": "^2.1.4",
13
- "@warlock.js/context": "5.1.0",
14
- "@warlock.js/logger": "5.1.0",
15
- "@warlock.js/seal": "5.1.0",
16
+ "@warlock.js/context": "5.2.3",
17
+ "@warlock.js/logger": "5.2.3",
18
+ "@warlock.js/seal": "5.2.3",
16
19
  "citty": "^0.2.2",
17
20
  "fast-glob": "^3.3.3"
18
21
  },
@@ -40,7 +43,7 @@
40
43
  ],
41
44
  "author": "hassanzohdy",
42
45
  "license": "MIT",
43
- "version": "5.1.0",
46
+ "version": "5.2.3",
44
47
  "main": "./cjs/index.cjs",
45
48
  "module": "./esm/index.mjs",
46
49
  "types": "./esm/index.d.mts",
@@ -9,10 +9,12 @@ Model-first TypeScript ORM for MongoDB and Postgres. Query straight off the mode
9
9
 
10
10
  > This skill is the cascade **map** — read it first, then load the specific skill for the task.
11
11
 
12
+ Server-only: `package.json` declares `"warlock": { "environment": "server" }`. `@warlock.js/web`'s build refuses app client code that value-imports this package; type-only imports are allowed, and server loaders/controllers/modules are unaffected.
13
+
12
14
  ## Install
13
15
 
14
16
  ```bash
15
- yarn add @warlock.js/cascade @warlock.js/seal
17
+ pnpm add @warlock.js/cascade @warlock.js/seal
16
18
  ```
17
19
 
18
20
  ## Foundations
@@ -52,10 +52,10 @@ See [`@warlock.js/cascade/configure-delete-strategy/SKILL.md`](@warlock.js/casca
52
52
  ## Running migrations
53
53
 
54
54
  ```bash
55
- yarn cascade migrate # apply pending migrations
56
- yarn cascade migrate:rollback # undo the last batch
57
- yarn cascade migrate:list # which migrations have been executed
58
- yarn cascade migrate:export-sql # write .up.sql / .down.sql instead of executing
55
+ pnpm cascade migrate # apply pending migrations
56
+ pnpm cascade migrate:rollback # undo the last batch
57
+ pnpm cascade migrate:list # which migrations have been executed
58
+ pnpm cascade migrate:export-sql # write .up.sql / .down.sql instead of executing
59
59
  ```
60
60
 
61
61
  `cascade migrate` discovers migration files via the `-p`/`--path` glob (default `./migrations/**`), runs them in order, and records each in the `_migrations` table / collection. See [`@warlock.js/cascade/run-cascade-cli/SKILL.md`](@warlock.js/cascade/run-cascade-cli/SKILL.md) for every flag and the programmatic Operations API.