@warlock.js/cascade 4.0.171 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/bin/cascadejs +3 -0
- package/esm/cli/commands/migrate-export-sql.mjs +48 -0
- package/esm/cli/commands/migrate-export-sql.mjs.map +1 -0
- package/esm/cli/commands/migrate-list.mjs +26 -0
- package/esm/cli/commands/migrate-list.mjs.map +1 -0
- package/esm/cli/commands/migrate-rollback.mjs +50 -0
- package/esm/cli/commands/migrate-rollback.mjs.map +1 -0
- package/esm/cli/commands/migrate.mjs +65 -0
- package/esm/cli/commands/migrate.mjs.map +1 -0
- package/esm/cli/connection-from-env.mjs +181 -0
- package/esm/cli/connection-from-env.mjs.map +1 -0
- package/esm/cli/index.mjs +31 -0
- package/esm/cli/index.mjs.map +1 -0
- package/esm/cli/load-migrations.mjs +78 -0
- package/esm/cli/load-migrations.mjs.map +1 -0
- package/esm/cli/printers.mjs +57 -0
- package/esm/cli/printers.mjs.map +1 -0
- package/esm/cli/setup-logger.mjs +30 -0
- package/esm/cli/setup-logger.mjs.map +1 -0
- package/esm/cli/with-cli-connection.mjs +39 -0
- package/esm/cli/with-cli-connection.mjs.map +1 -0
- package/esm/cli.d.mts +1 -0
- package/esm/cli.mjs +26 -0
- package/esm/cli.mjs.map +1 -0
- package/esm/context/database-data-source-context.d.mts +32 -0
- package/esm/context/database-data-source-context.d.mts.map +1 -0
- package/esm/context/database-data-source-context.mjs +35 -0
- package/esm/context/database-data-source-context.mjs.map +1 -0
- package/esm/context/database-transaction-context.d.mts +38 -0
- package/esm/context/database-transaction-context.d.mts.map +1 -0
- package/esm/context/database-transaction-context.mjs +47 -0
- package/esm/context/database-transaction-context.mjs.map +1 -0
- package/esm/contracts/database-driver.contract.d.mts +443 -0
- package/esm/contracts/database-driver.contract.d.mts.map +1 -0
- package/esm/contracts/database-id-generator.contract.d.mts +109 -0
- package/esm/contracts/database-id-generator.contract.d.mts.map +1 -0
- package/esm/contracts/database-remover.contract.d.mts +108 -0
- package/esm/contracts/database-remover.contract.d.mts.map +1 -0
- package/esm/contracts/database-restorer.contract.d.mts +145 -0
- package/esm/contracts/database-restorer.contract.d.mts.map +1 -0
- package/esm/contracts/database-writer.contract.d.mts +123 -0
- package/esm/contracts/database-writer.contract.d.mts.map +1 -0
- package/esm/contracts/driver-blueprint.contract.d.mts +52 -0
- package/esm/contracts/driver-blueprint.contract.d.mts.map +1 -0
- package/esm/contracts/index.d.mts +9 -0
- package/esm/contracts/migration-driver.contract.d.mts +476 -0
- package/esm/contracts/migration-driver.contract.d.mts.map +1 -0
- package/esm/contracts/query-builder.contract.d.mts +1663 -0
- package/esm/contracts/query-builder.contract.d.mts.map +1 -0
- package/esm/contracts/sync-adapter.contract.d.mts +49 -0
- package/esm/contracts/sync-adapter.contract.d.mts.map +1 -0
- package/esm/data-source/data-source-registry.d.mts +111 -0
- package/esm/data-source/data-source-registry.d.mts.map +1 -0
- package/esm/data-source/data-source-registry.mjs +142 -0
- package/esm/data-source/data-source-registry.mjs.map +1 -0
- package/esm/data-source/data-source.d.mts +160 -0
- package/esm/data-source/data-source.d.mts.map +1 -0
- package/esm/data-source/data-source.mjs +87 -0
- package/esm/data-source/data-source.mjs.map +1 -0
- package/esm/database-dirty-tracker.d.mts +254 -0
- package/esm/database-dirty-tracker.d.mts.map +1 -0
- package/esm/database-dirty-tracker.mjs +356 -0
- package/esm/database-dirty-tracker.mjs.map +1 -0
- package/esm/drivers/mongodb/mongodb-blueprint.mjs +54 -0
- package/esm/drivers/mongodb/mongodb-blueprint.mjs.map +1 -0
- package/esm/drivers/mongodb/mongodb-driver.d.mts +334 -0
- package/esm/drivers/mongodb/mongodb-driver.d.mts.map +1 -0
- package/esm/drivers/mongodb/mongodb-driver.mjs +716 -0
- package/esm/drivers/mongodb/mongodb-driver.mjs.map +1 -0
- package/esm/drivers/mongodb/mongodb-id-generator.d.mts +120 -0
- package/esm/drivers/mongodb/mongodb-id-generator.d.mts.map +1 -0
- package/esm/drivers/mongodb/mongodb-id-generator.mjs +141 -0
- package/esm/drivers/mongodb/mongodb-id-generator.mjs.map +1 -0
- package/esm/drivers/mongodb/mongodb-migration-driver.d.mts +322 -0
- package/esm/drivers/mongodb/mongodb-migration-driver.d.mts.map +1 -0
- package/esm/drivers/mongodb/mongodb-migration-driver.mjs +531 -0
- package/esm/drivers/mongodb/mongodb-migration-driver.mjs.map +1 -0
- package/esm/drivers/mongodb/mongodb-query-builder.d.mts +1117 -0
- package/esm/drivers/mongodb/mongodb-query-builder.d.mts.map +1 -0
- package/esm/drivers/mongodb/mongodb-query-builder.mjs +1828 -0
- package/esm/drivers/mongodb/mongodb-query-builder.mjs.map +1 -0
- package/esm/drivers/mongodb/mongodb-query-operations.d.mts +230 -0
- package/esm/drivers/mongodb/mongodb-query-operations.d.mts.map +1 -0
- package/esm/drivers/mongodb/mongodb-query-operations.mjs +275 -0
- package/esm/drivers/mongodb/mongodb-query-operations.mjs.map +1 -0
- package/esm/drivers/mongodb/mongodb-query-parser.d.mts +263 -0
- package/esm/drivers/mongodb/mongodb-query-parser.d.mts.map +1 -0
- package/esm/drivers/mongodb/mongodb-query-parser.mjs +965 -0
- package/esm/drivers/mongodb/mongodb-query-parser.mjs.map +1 -0
- package/esm/drivers/mongodb/mongodb-sync-adapter.d.mts +78 -0
- package/esm/drivers/mongodb/mongodb-sync-adapter.d.mts.map +1 -0
- package/esm/drivers/mongodb/mongodb-sync-adapter.mjs +118 -0
- package/esm/drivers/mongodb/mongodb-sync-adapter.mjs.map +1 -0
- package/esm/drivers/mongodb/types.d.mts +43 -0
- package/esm/drivers/mongodb/types.d.mts.map +1 -0
- package/esm/drivers/postgres/index.d.mts +8 -0
- package/esm/drivers/postgres/index.mjs +9 -0
- package/esm/drivers/postgres/postgres-blueprint.d.mts +60 -0
- package/esm/drivers/postgres/postgres-blueprint.d.mts.map +1 -0
- package/esm/drivers/postgres/postgres-blueprint.mjs +105 -0
- package/esm/drivers/postgres/postgres-blueprint.mjs.map +1 -0
- package/esm/drivers/postgres/postgres-dialect.d.mts +144 -0
- package/esm/drivers/postgres/postgres-dialect.d.mts.map +1 -0
- package/esm/drivers/postgres/postgres-dialect.mjs +227 -0
- package/esm/drivers/postgres/postgres-dialect.mjs.map +1 -0
- package/esm/drivers/postgres/postgres-driver.d.mts +424 -0
- package/esm/drivers/postgres/postgres-driver.d.mts.map +1 -0
- package/esm/drivers/postgres/postgres-driver.mjs +845 -0
- package/esm/drivers/postgres/postgres-driver.mjs.map +1 -0
- package/esm/drivers/postgres/postgres-migration-driver.d.mts +393 -0
- package/esm/drivers/postgres/postgres-migration-driver.d.mts.map +1 -0
- package/esm/drivers/postgres/postgres-migration-driver.mjs +760 -0
- package/esm/drivers/postgres/postgres-migration-driver.mjs.map +1 -0
- package/esm/drivers/postgres/postgres-query-builder.d.mts +399 -0
- package/esm/drivers/postgres/postgres-query-builder.d.mts.map +1 -0
- package/esm/drivers/postgres/postgres-query-builder.mjs +1105 -0
- package/esm/drivers/postgres/postgres-query-builder.mjs.map +1 -0
- package/esm/drivers/postgres/postgres-query-parser.d.mts +351 -0
- package/esm/drivers/postgres/postgres-query-parser.d.mts.map +1 -0
- package/esm/drivers/postgres/postgres-query-parser.mjs +796 -0
- package/esm/drivers/postgres/postgres-query-parser.mjs.map +1 -0
- package/esm/drivers/postgres/postgres-sql-serializer.mjs +260 -0
- package/esm/drivers/postgres/postgres-sql-serializer.mjs.map +1 -0
- package/esm/drivers/postgres/postgres-sync-adapter.d.mts +79 -0
- package/esm/drivers/postgres/postgres-sync-adapter.d.mts.map +1 -0
- package/esm/drivers/postgres/postgres-sync-adapter.mjs +162 -0
- package/esm/drivers/postgres/postgres-sync-adapter.mjs.map +1 -0
- package/esm/drivers/postgres/types.d.mts +105 -0
- package/esm/drivers/postgres/types.d.mts.map +1 -0
- package/esm/drivers/sql/sql-dialect.contract.d.mts +221 -0
- package/esm/drivers/sql/sql-dialect.contract.d.mts.map +1 -0
- package/esm/drivers/sql/sql-types.d.mts +150 -0
- package/esm/drivers/sql/sql-types.d.mts.map +1 -0
- package/esm/errors/missing-data-source.error.d.mts +25 -0
- package/esm/errors/missing-data-source.error.d.mts.map +1 -0
- package/esm/errors/missing-data-source.error.mjs +31 -0
- package/esm/errors/missing-data-source.error.mjs.map +1 -0
- package/esm/errors/transaction-rollback.error.d.mts +23 -0
- package/esm/errors/transaction-rollback.error.d.mts.map +1 -0
- package/esm/errors/transaction-rollback.error.mjs +29 -0
- package/esm/errors/transaction-rollback.error.mjs.map +1 -0
- package/esm/events/model-events.d.mts +234 -0
- package/esm/events/model-events.d.mts.map +1 -0
- package/esm/events/model-events.mjs +254 -0
- package/esm/events/model-events.mjs.map +1 -0
- package/esm/expressions/aggregate-expressions.d.mts +224 -0
- package/esm/expressions/aggregate-expressions.d.mts.map +1 -0
- package/esm/expressions/aggregate-expressions.mjs +232 -0
- package/esm/expressions/aggregate-expressions.mjs.map +1 -0
- package/esm/index.d.mts +67 -0
- package/esm/index.mjs +53 -0
- package/esm/migration/column-builder.d.mts +420 -0
- package/esm/migration/column-builder.d.mts.map +1 -0
- package/esm/migration/column-builder.mjs +532 -0
- package/esm/migration/column-builder.mjs.map +1 -0
- package/esm/migration/column-helpers.d.mts +280 -0
- package/esm/migration/column-helpers.d.mts.map +1 -0
- package/esm/migration/column-helpers.mjs +376 -0
- package/esm/migration/column-helpers.mjs.map +1 -0
- package/esm/migration/foreign-key-builder.d.mts +106 -0
- package/esm/migration/foreign-key-builder.d.mts.map +1 -0
- package/esm/migration/foreign-key-builder.mjs +126 -0
- package/esm/migration/foreign-key-builder.mjs.map +1 -0
- package/esm/migration/index.d.mts +6 -0
- package/esm/migration/index.mjs +7 -0
- package/esm/migration/migration-runner.d.mts +279 -0
- package/esm/migration/migration-runner.d.mts.map +1 -0
- package/esm/migration/migration-runner.mjs +662 -0
- package/esm/migration/migration-runner.mjs.map +1 -0
- package/esm/migration/migration.d.mts +2035 -0
- package/esm/migration/migration.d.mts.map +1 -0
- package/esm/migration/migration.mjs +2083 -0
- package/esm/migration/migration.mjs.map +1 -0
- package/esm/migration/sql-grammar.mjs +115 -0
- package/esm/migration/sql-grammar.mjs.map +1 -0
- package/esm/migration/sql-serializer.d.mts +26 -0
- package/esm/migration/sql-serializer.d.mts.map +1 -0
- package/esm/migration/sql-serializer.mjs +26 -0
- package/esm/migration/sql-serializer.mjs.map +1 -0
- package/esm/migration/types.d.mts +136 -0
- package/esm/migration/types.d.mts.map +1 -0
- package/esm/model/methods/accessor-methods.mjs +54 -0
- package/esm/model/methods/accessor-methods.mjs.map +1 -0
- package/esm/model/methods/delete-methods.mjs +16 -0
- package/esm/model/methods/delete-methods.mjs.map +1 -0
- package/esm/model/methods/dirty-methods.mjs +20 -0
- package/esm/model/methods/dirty-methods.mjs.map +1 -0
- package/esm/model/methods/hydration-methods.mjs +51 -0
- package/esm/model/methods/hydration-methods.mjs.map +1 -0
- package/esm/model/methods/instance-event-methods.mjs +22 -0
- package/esm/model/methods/instance-event-methods.mjs.map +1 -0
- package/esm/model/methods/meta-methods.mjs +36 -0
- package/esm/model/methods/meta-methods.mjs.map +1 -0
- package/esm/model/methods/pivot-methods.mjs +48 -0
- package/esm/model/methods/pivot-methods.mjs.map +1 -0
- package/esm/model/methods/query-methods.mjs +121 -0
- package/esm/model/methods/query-methods.mjs.map +1 -0
- package/esm/model/methods/restore-methods.mjs +16 -0
- package/esm/model/methods/restore-methods.mjs.map +1 -0
- package/esm/model/methods/scope-methods.mjs +20 -0
- package/esm/model/methods/scope-methods.mjs.map +1 -0
- package/esm/model/methods/serialization-methods.mjs +20 -0
- package/esm/model/methods/serialization-methods.mjs.map +1 -0
- package/esm/model/methods/static-event-methods.mjs +37 -0
- package/esm/model/methods/static-event-methods.mjs.map +1 -0
- package/esm/model/methods/write-methods.mjs +69 -0
- package/esm/model/methods/write-methods.mjs.map +1 -0
- package/esm/model/model.d.mts +1778 -0
- package/esm/model/model.d.mts.map +1 -0
- package/esm/model/model.mjs +1762 -0
- package/esm/model/model.mjs.map +1 -0
- package/esm/model/model.types.d.mts +47 -0
- package/esm/model/model.types.d.mts.map +1 -0
- package/esm/model/register-model.d.mts +140 -0
- package/esm/model/register-model.d.mts.map +1 -0
- package/esm/model/register-model.mjs +175 -0
- package/esm/model/register-model.mjs.map +1 -0
- package/esm/model/relation-decorators.d.mts +88 -0
- package/esm/model/relation-decorators.d.mts.map +1 -0
- package/esm/model/relation-decorators.mjs +191 -0
- package/esm/model/relation-decorators.mjs.map +1 -0
- package/esm/operations/database.d.mts +46 -0
- package/esm/operations/database.d.mts.map +1 -0
- package/esm/operations/database.mjs +40 -0
- package/esm/operations/database.mjs.map +1 -0
- package/esm/operations/index.d.mts +2 -0
- package/esm/operations/index.mjs +4 -0
- package/esm/operations/migrations.d.mts +71 -0
- package/esm/operations/migrations.d.mts.map +1 -0
- package/esm/operations/migrations.mjs +70 -0
- package/esm/operations/migrations.mjs.map +1 -0
- package/esm/query-builder/query-builder.d.mts +564 -0
- package/esm/query-builder/query-builder.d.mts.map +1 -0
- package/esm/query-builder/query-builder.mjs +1097 -0
- package/esm/query-builder/query-builder.mjs.map +1 -0
- package/esm/relations/index.d.mts +4 -0
- package/esm/relations/index.mjs +5 -0
- package/esm/relations/key-conventions.mjs +119 -0
- package/esm/relations/key-conventions.mjs.map +1 -0
- package/esm/relations/pivot-operations.d.mts +155 -0
- package/esm/relations/pivot-operations.d.mts.map +1 -0
- package/esm/relations/pivot-operations.mjs +232 -0
- package/esm/relations/pivot-operations.mjs.map +1 -0
- package/esm/relations/relation-hydrator.d.mts +55 -0
- package/esm/relations/relation-hydrator.d.mts.map +1 -0
- package/esm/relations/relation-hydrator.mjs +52 -0
- package/esm/relations/relation-hydrator.mjs.map +1 -0
- package/esm/relations/relation-loader.d.mts +190 -0
- package/esm/relations/relation-loader.d.mts.map +1 -0
- package/esm/relations/relation-loader.mjs +416 -0
- package/esm/relations/relation-loader.mjs.map +1 -0
- package/esm/relations/types.d.mts +317 -0
- package/esm/relations/types.d.mts.map +1 -0
- package/esm/remover/database-remover.d.mts +104 -0
- package/esm/remover/database-remover.d.mts.map +1 -0
- package/esm/remover/database-remover.mjs +174 -0
- package/esm/remover/database-remover.mjs.map +1 -0
- package/esm/restorer/database-restorer.d.mts +135 -0
- package/esm/restorer/database-restorer.d.mts.map +1 -0
- package/esm/restorer/database-restorer.mjs +316 -0
- package/esm/restorer/database-restorer.mjs.map +1 -0
- package/esm/sql-database-dirty-tracker.d.mts +17 -0
- package/esm/sql-database-dirty-tracker.d.mts.map +1 -0
- package/esm/sql-database-dirty-tracker.mjs +20 -0
- package/esm/sql-database-dirty-tracker.mjs.map +1 -0
- package/esm/sync/model-events.mjs +46 -0
- package/esm/sync/model-events.mjs.map +1 -0
- package/esm/sync/model-sync-operation.d.mts +159 -0
- package/esm/sync/model-sync-operation.d.mts.map +1 -0
- package/esm/sync/model-sync-operation.mjs +257 -0
- package/esm/sync/model-sync-operation.mjs.map +1 -0
- package/esm/sync/model-sync.d.mts +126 -0
- package/esm/sync/model-sync.d.mts.map +1 -0
- package/esm/sync/model-sync.mjs +157 -0
- package/esm/sync/model-sync.mjs.map +1 -0
- package/esm/sync/sync-context.d.mts +69 -0
- package/esm/sync/sync-context.d.mts.map +1 -0
- package/esm/sync/sync-context.mjs +95 -0
- package/esm/sync/sync-context.mjs.map +1 -0
- package/esm/sync/sync-manager.d.mts +213 -0
- package/esm/sync/sync-manager.d.mts.map +1 -0
- package/esm/sync/sync-manager.mjs +597 -0
- package/esm/sync/sync-manager.mjs.map +1 -0
- package/esm/sync/types.d.mts +215 -0
- package/esm/sync/types.d.mts.map +1 -0
- package/esm/types.d.mts +423 -0
- package/esm/types.d.mts.map +1 -0
- package/esm/utils/connect-to-database.d.mts +328 -0
- package/esm/utils/connect-to-database.d.mts.map +1 -0
- package/esm/utils/connect-to-database.mjs +130 -0
- package/esm/utils/connect-to-database.mjs.map +1 -0
- package/esm/utils/database-writer.utils.d.mts +18 -0
- package/esm/utils/database-writer.utils.d.mts.map +1 -0
- package/esm/utils/database-writer.utils.mjs +25 -0
- package/esm/utils/database-writer.utils.mjs.map +1 -0
- package/esm/utils/define-model.d.mts +185 -0
- package/esm/utils/define-model.d.mts.map +1 -0
- package/esm/utils/define-model.mjs +105 -0
- package/esm/utils/define-model.mjs.map +1 -0
- package/esm/utils/is-valid-date-value.mjs +22 -0
- package/esm/utils/is-valid-date-value.mjs.map +1 -0
- package/esm/utils/once-connected.d.mts +150 -0
- package/esm/utils/once-connected.d.mts.map +1 -0
- package/esm/utils/once-connected.mjs +203 -0
- package/esm/utils/once-connected.mjs.map +1 -0
- package/esm/validation/database-seal-plugins.d.mts +1 -0
- package/esm/validation/database-seal-plugins.mjs +11 -0
- package/esm/validation/database-seal-plugins.mjs.map +1 -0
- package/esm/validation/database-writer-validation-error.d.mts +101 -0
- package/esm/validation/database-writer-validation-error.d.mts.map +1 -0
- package/esm/validation/database-writer-validation-error.mjs +153 -0
- package/esm/validation/database-writer-validation-error.mjs.map +1 -0
- package/esm/validation/index.d.mts +2 -0
- package/esm/validation/index.mjs +4 -0
- package/esm/validation/mutators/embed-mutator.mjs +26 -0
- package/esm/validation/mutators/embed-mutator.mjs.map +1 -0
- package/esm/validation/plugins/database-rules-plugin.d.mts +28 -0
- package/esm/validation/plugins/database-rules-plugin.d.mts.map +1 -0
- package/esm/validation/plugins/database-rules-plugin.mjs +43 -0
- package/esm/validation/plugins/database-rules-plugin.mjs.map +1 -0
- package/esm/validation/plugins/embed-validator-plugin.d.mts +17 -0
- package/esm/validation/plugins/embed-validator-plugin.d.mts.map +1 -0
- package/esm/validation/plugins/embed-validator-plugin.mjs +20 -0
- package/esm/validation/plugins/embed-validator-plugin.mjs.map +1 -0
- package/esm/validation/rules/database-model-rule.mjs +32 -0
- package/esm/validation/rules/database-model-rule.mjs.map +1 -0
- package/esm/validation/rules/exists-rule.mjs +29 -0
- package/esm/validation/rules/exists-rule.mjs.map +1 -0
- package/esm/validation/rules/unique-rule.mjs +43 -0
- package/esm/validation/rules/unique-rule.mjs.map +1 -0
- package/esm/validation/transformers/embed-model-transformer.mjs +17 -0
- package/esm/validation/transformers/embed-model-transformer.mjs.map +1 -0
- package/esm/validation/types.d.mts +43 -0
- package/esm/validation/types.d.mts.map +1 -0
- package/esm/validation/validators/embed-validator.d.mts +25 -0
- package/esm/validation/validators/embed-validator.d.mts.map +1 -0
- package/esm/validation/validators/embed-validator.mjs +42 -0
- package/esm/validation/validators/embed-validator.mjs.map +1 -0
- package/esm/writer/database-writer.d.mts +178 -0
- package/esm/writer/database-writer.d.mts.map +1 -0
- package/esm/writer/database-writer.mjs +317 -0
- package/esm/writer/database-writer.mjs.map +1 -0
- package/llms-full.txt +2027 -0
- package/llms.txt +23 -0
- package/package.json +60 -51
- package/skills/README.md +65 -0
- package/skills/aggregate-data/SKILL.md +102 -0
- package/skills/cascade-basics/SKILL.md +93 -0
- package/skills/configure-delete-strategy/SKILL.md +126 -0
- package/skills/define-model/SKILL.md +170 -0
- package/skills/define-relations/SKILL.md +171 -0
- package/skills/manage-data-sources/SKILL.md +140 -0
- package/skills/manage-transactions/SKILL.md +118 -0
- package/skills/paginate-results/SKILL.md +122 -0
- package/skills/perform-atomic-ops/SKILL.md +98 -0
- package/skills/query-data/SKILL.md +168 -0
- package/skills/run-cascade-cli/SKILL.md +125 -0
- package/skills/search-by-vector/SKILL.md +127 -0
- package/skills/subscribe-to-model-events/SKILL.md +148 -0
- package/skills/track-changes/SKILL.md +109 -0
- package/skills/write-migration/SKILL.md +144 -0
- package/cjs/context/database-data-source-context.d.ts +0 -29
- package/cjs/context/database-data-source-context.d.ts.map +0 -1
- package/cjs/context/database-data-source-context.js +0 -28
- package/cjs/context/database-data-source-context.js.map +0 -1
- package/cjs/context/database-transaction-context.d.ts +0 -35
- package/cjs/context/database-transaction-context.d.ts.map +0 -1
- package/cjs/context/database-transaction-context.js +0 -40
- package/cjs/context/database-transaction-context.js.map +0 -1
- package/cjs/contracts/database-driver.contract.d.ts +0 -450
- package/cjs/contracts/database-driver.contract.d.ts.map +0 -1
- package/cjs/contracts/database-id-generator.contract.d.ts +0 -109
- package/cjs/contracts/database-id-generator.contract.d.ts.map +0 -1
- package/cjs/contracts/database-remover.contract.d.ts +0 -104
- package/cjs/contracts/database-remover.contract.d.ts.map +0 -1
- package/cjs/contracts/database-restorer.contract.d.ts +0 -143
- package/cjs/contracts/database-restorer.contract.d.ts.map +0 -1
- package/cjs/contracts/database-writer.contract.d.ts +0 -119
- package/cjs/contracts/database-writer.contract.d.ts.map +0 -1
- package/cjs/contracts/driver-blueprint.contract.d.ts +0 -49
- package/cjs/contracts/driver-blueprint.contract.d.ts.map +0 -1
- package/cjs/contracts/index.d.ts +0 -10
- package/cjs/contracts/index.d.ts.map +0 -1
- package/cjs/contracts/migration-driver.contract.d.ts +0 -522
- package/cjs/contracts/migration-driver.contract.d.ts.map +0 -1
- package/cjs/contracts/query-builder.contract.d.ts +0 -1609
- package/cjs/contracts/query-builder.contract.d.ts.map +0 -1
- package/cjs/contracts/sync-adapter.contract.d.ts +0 -58
- package/cjs/contracts/sync-adapter.contract.d.ts.map +0 -1
- package/cjs/data-source/data-source-registry.d.ts +0 -108
- package/cjs/data-source/data-source-registry.d.ts.map +0 -1
- package/cjs/data-source/data-source-registry.js +0 -145
- package/cjs/data-source/data-source-registry.js.map +0 -1
- package/cjs/data-source/data-source.d.ts +0 -147
- package/cjs/data-source/data-source.d.ts.map +0 -1
- package/cjs/data-source/data-source.js +0 -83
- package/cjs/data-source/data-source.js.map +0 -1
- package/cjs/database-dirty-tracker.d.ts +0 -252
- package/cjs/database-dirty-tracker.d.ts.map +0 -1
- package/cjs/database-dirty-tracker.js +0 -386
- package/cjs/database-dirty-tracker.js.map +0 -1
- package/cjs/drivers/mongodb/mongodb-blueprint.d.ts +0 -30
- package/cjs/drivers/mongodb/mongodb-blueprint.d.ts.map +0 -1
- package/cjs/drivers/mongodb/mongodb-blueprint.js +0 -51
- package/cjs/drivers/mongodb/mongodb-blueprint.js.map +0 -1
- package/cjs/drivers/mongodb/mongodb-driver.d.ts +0 -325
- package/cjs/drivers/mongodb/mongodb-driver.d.ts.map +0 -1
- package/cjs/drivers/mongodb/mongodb-driver.js +0 -845
- package/cjs/drivers/mongodb/mongodb-driver.js.map +0 -1
- package/cjs/drivers/mongodb/mongodb-id-generator.d.ts +0 -116
- package/cjs/drivers/mongodb/mongodb-id-generator.d.ts.map +0 -1
- package/cjs/drivers/mongodb/mongodb-id-generator.js +0 -149
- package/cjs/drivers/mongodb/mongodb-id-generator.js.map +0 -1
- package/cjs/drivers/mongodb/mongodb-migration-driver.d.ts +0 -317
- package/cjs/drivers/mongodb/mongodb-migration-driver.d.ts.map +0 -1
- package/cjs/drivers/mongodb/mongodb-migration-driver.js +0 -666
- package/cjs/drivers/mongodb/mongodb-migration-driver.js.map +0 -1
- package/cjs/drivers/mongodb/mongodb-query-builder.d.ts +0 -1122
- package/cjs/drivers/mongodb/mongodb-query-builder.d.ts.map +0 -1
- package/cjs/drivers/mongodb/mongodb-query-builder.js +0 -1988
- package/cjs/drivers/mongodb/mongodb-query-builder.js.map +0 -1
- package/cjs/drivers/mongodb/mongodb-query-operations.d.ts +0 -226
- package/cjs/drivers/mongodb/mongodb-query-operations.d.ts.map +0 -1
- package/cjs/drivers/mongodb/mongodb-query-operations.js +0 -270
- package/cjs/drivers/mongodb/mongodb-query-operations.js.map +0 -1
- package/cjs/drivers/mongodb/mongodb-query-parser.d.ts +0 -262
- package/cjs/drivers/mongodb/mongodb-query-parser.d.ts.map +0 -1
- package/cjs/drivers/mongodb/mongodb-query-parser.js +0 -1351
- package/cjs/drivers/mongodb/mongodb-query-parser.js.map +0 -1
- package/cjs/drivers/mongodb/mongodb-sync-adapter.d.ts +0 -79
- package/cjs/drivers/mongodb/mongodb-sync-adapter.d.ts.map +0 -1
- package/cjs/drivers/mongodb/mongodb-sync-adapter.js +0 -146
- package/cjs/drivers/mongodb/mongodb-sync-adapter.js.map +0 -1
- package/cjs/drivers/mongodb/types.d.ts +0 -43
- package/cjs/drivers/mongodb/types.d.ts.map +0 -1
- package/cjs/drivers/postgres/index.d.ts +0 -16
- package/cjs/drivers/postgres/index.d.ts.map +0 -1
- package/cjs/drivers/postgres/postgres-blueprint.d.ts +0 -64
- package/cjs/drivers/postgres/postgres-blueprint.d.ts.map +0 -1
- package/cjs/drivers/postgres/postgres-blueprint.js +0 -121
- package/cjs/drivers/postgres/postgres-blueprint.js.map +0 -1
- package/cjs/drivers/postgres/postgres-dialect.d.ts +0 -136
- package/cjs/drivers/postgres/postgres-dialect.d.ts.map +0 -1
- package/cjs/drivers/postgres/postgres-dialect.js +0 -268
- package/cjs/drivers/postgres/postgres-dialect.js.map +0 -1
- package/cjs/drivers/postgres/postgres-driver.d.ts +0 -432
- package/cjs/drivers/postgres/postgres-driver.d.ts.map +0 -1
- package/cjs/drivers/postgres/postgres-driver.js +0 -1008
- package/cjs/drivers/postgres/postgres-driver.js.map +0 -1
- package/cjs/drivers/postgres/postgres-migration-driver.d.ts +0 -397
- package/cjs/drivers/postgres/postgres-migration-driver.d.ts.map +0 -1
- package/cjs/drivers/postgres/postgres-migration-driver.js +0 -900
- package/cjs/drivers/postgres/postgres-migration-driver.js.map +0 -1
- package/cjs/drivers/postgres/postgres-query-builder.d.ts +0 -254
- package/cjs/drivers/postgres/postgres-query-builder.d.ts.map +0 -1
- package/cjs/drivers/postgres/postgres-query-builder.js +0 -933
- package/cjs/drivers/postgres/postgres-query-builder.js.map +0 -1
- package/cjs/drivers/postgres/postgres-query-parser.d.ts +0 -328
- package/cjs/drivers/postgres/postgres-query-parser.d.ts.map +0 -1
- package/cjs/drivers/postgres/postgres-query-parser.js +0 -868
- package/cjs/drivers/postgres/postgres-query-parser.js.map +0 -1
- package/cjs/drivers/postgres/postgres-sql-serializer.d.ts +0 -37
- package/cjs/drivers/postgres/postgres-sql-serializer.d.ts.map +0 -1
- package/cjs/drivers/postgres/postgres-sql-serializer.js +0 -400
- package/cjs/drivers/postgres/postgres-sql-serializer.js.map +0 -1
- package/cjs/drivers/postgres/postgres-sync-adapter.d.ts +0 -83
- package/cjs/drivers/postgres/postgres-sync-adapter.d.ts.map +0 -1
- package/cjs/drivers/postgres/postgres-sync-adapter.js +0 -204
- package/cjs/drivers/postgres/postgres-sync-adapter.js.map +0 -1
- package/cjs/drivers/postgres/types.d.ts +0 -144
- package/cjs/drivers/postgres/types.d.ts.map +0 -1
- package/cjs/drivers/sql/index.d.ts +0 -10
- package/cjs/drivers/sql/index.d.ts.map +0 -1
- package/cjs/drivers/sql/sql-dialect.contract.d.ts +0 -204
- package/cjs/drivers/sql/sql-dialect.contract.d.ts.map +0 -1
- package/cjs/drivers/sql/sql-types.d.ts +0 -202
- package/cjs/drivers/sql/sql-types.d.ts.map +0 -1
- package/cjs/errors/missing-data-source.error.d.ts +0 -22
- package/cjs/errors/missing-data-source.error.d.ts.map +0 -1
- package/cjs/errors/missing-data-source.error.js +0 -29
- package/cjs/errors/missing-data-source.error.js.map +0 -1
- package/cjs/errors/transaction-rollback.error.d.ts +0 -20
- package/cjs/errors/transaction-rollback.error.d.ts.map +0 -1
- package/cjs/errors/transaction-rollback.error.js +0 -27
- package/cjs/errors/transaction-rollback.error.js.map +0 -1
- package/cjs/events/model-events.d.ts +0 -231
- package/cjs/events/model-events.d.ts.map +0 -1
- package/cjs/events/model-events.js +0 -259
- package/cjs/events/model-events.js.map +0 -1
- package/cjs/expressions/aggregate-expressions.d.ts +0 -215
- package/cjs/expressions/aggregate-expressions.d.ts.map +0 -1
- package/cjs/expressions/aggregate-expressions.js +0 -221
- package/cjs/expressions/aggregate-expressions.js.map +0 -1
- package/cjs/expressions/index.d.ts +0 -2
- package/cjs/expressions/index.d.ts.map +0 -1
- package/cjs/index.d.ts +0 -45
- package/cjs/index.d.ts.map +0 -1
- package/cjs/index.js +0 -1
- package/cjs/index.js.map +0 -1
- package/cjs/migration/column-builder.d.ts +0 -417
- package/cjs/migration/column-builder.d.ts.map +0 -1
- package/cjs/migration/column-builder.js +0 -586
- package/cjs/migration/column-builder.js.map +0 -1
- package/cjs/migration/column-helpers.d.ts +0 -275
- package/cjs/migration/column-helpers.d.ts.map +0 -1
- package/cjs/migration/column-helpers.js +0 -389
- package/cjs/migration/column-helpers.js.map +0 -1
- package/cjs/migration/foreign-key-builder.d.ts +0 -103
- package/cjs/migration/foreign-key-builder.d.ts.map +0 -1
- package/cjs/migration/foreign-key-builder.js +0 -121
- package/cjs/migration/foreign-key-builder.js.map +0 -1
- package/cjs/migration/index.d.ts +0 -7
- package/cjs/migration/index.d.ts.map +0 -1
- package/cjs/migration/migration-runner.d.ts +0 -278
- package/cjs/migration/migration-runner.d.ts.map +0 -1
- package/cjs/migration/migration-runner.js +0 -815
- package/cjs/migration/migration-runner.js.map +0 -1
- package/cjs/migration/migration.d.ts +0 -1988
- package/cjs/migration/migration.d.ts.map +0 -1
- package/cjs/migration/migration.js +0 -2162
- package/cjs/migration/migration.js.map +0 -1
- package/cjs/migration/sql-grammar.d.ts +0 -61
- package/cjs/migration/sql-grammar.d.ts.map +0 -1
- package/cjs/migration/sql-grammar.js +0 -164
- package/cjs/migration/sql-grammar.js.map +0 -1
- package/cjs/migration/sql-serializer.d.ts +0 -22
- package/cjs/migration/sql-serializer.d.ts.map +0 -1
- package/cjs/migration/sql-serializer.js +0 -26
- package/cjs/migration/sql-serializer.js.map +0 -1
- package/cjs/migration/types.d.ts +0 -155
- package/cjs/migration/types.d.ts.map +0 -1
- package/cjs/model/methods/accessor-methods.d.ts +0 -13
- package/cjs/model/methods/accessor-methods.d.ts.map +0 -1
- package/cjs/model/methods/accessor-methods.js +0 -51
- package/cjs/model/methods/accessor-methods.js.map +0 -1
- package/cjs/model/methods/delete-methods.d.ts +0 -10
- package/cjs/model/methods/delete-methods.d.ts.map +0 -1
- package/cjs/model/methods/delete-methods.js +0 -10
- package/cjs/model/methods/delete-methods.js.map +0 -1
- package/cjs/model/methods/dirty-methods.d.ts +0 -10
- package/cjs/model/methods/dirty-methods.d.ts.map +0 -1
- package/cjs/model/methods/dirty-methods.js +0 -15
- package/cjs/model/methods/dirty-methods.js.map +0 -1
- package/cjs/model/methods/hydration-methods.d.ts +0 -10
- package/cjs/model/methods/hydration-methods.d.ts.map +0 -1
- package/cjs/model/methods/hydration-methods.js +0 -57
- package/cjs/model/methods/hydration-methods.js.map +0 -1
- package/cjs/model/methods/instance-event-methods.d.ts +0 -7
- package/cjs/model/methods/instance-event-methods.d.ts.map +0 -1
- package/cjs/model/methods/instance-event-methods.js +0 -15
- package/cjs/model/methods/instance-event-methods.js.map +0 -1
- package/cjs/model/methods/meta-methods.d.ts +0 -7
- package/cjs/model/methods/meta-methods.d.ts.map +0 -1
- package/cjs/model/methods/meta-methods.js +0 -78
- package/cjs/model/methods/meta-methods.js.map +0 -1
- package/cjs/model/methods/query-methods.d.ts +0 -24
- package/cjs/model/methods/query-methods.d.ts.map +0 -1
- package/cjs/model/methods/query-methods.js +0 -164
- package/cjs/model/methods/query-methods.js.map +0 -1
- package/cjs/model/methods/restore-methods.d.ts +0 -10
- package/cjs/model/methods/restore-methods.d.ts.map +0 -1
- package/cjs/model/methods/restore-methods.js +0 -13
- package/cjs/model/methods/restore-methods.js.map +0 -1
- package/cjs/model/methods/scope-methods.d.ts +0 -7
- package/cjs/model/methods/scope-methods.d.ts.map +0 -1
- package/cjs/model/methods/scope-methods.js +0 -15
- package/cjs/model/methods/scope-methods.js.map +0 -1
- package/cjs/model/methods/serialization-methods.d.ts +0 -3
- package/cjs/model/methods/serialization-methods.d.ts.map +0 -1
- package/cjs/model/methods/serialization-methods.js +0 -27
- package/cjs/model/methods/serialization-methods.js.map +0 -1
- package/cjs/model/methods/static-event-methods.d.ts +0 -9
- package/cjs/model/methods/static-event-methods.d.ts.map +0 -1
- package/cjs/model/methods/static-event-methods.js +0 -29
- package/cjs/model/methods/static-event-methods.js.map +0 -1
- package/cjs/model/methods/write-methods.d.ts +0 -10
- package/cjs/model/methods/write-methods.d.ts.map +0 -1
- package/cjs/model/methods/write-methods.js +0 -52
- package/cjs/model/methods/write-methods.js.map +0 -1
- package/cjs/model/model.d.ts +0 -1647
- package/cjs/model/model.d.ts.map +0 -1
- package/cjs/model/model.js +0 -1657
- package/cjs/model/model.js.map +0 -1
- package/cjs/model/model.types.d.ts +0 -44
- package/cjs/model/model.types.d.ts.map +0 -1
- package/cjs/model/register-model.d.ts +0 -81
- package/cjs/model/register-model.d.ts.map +0 -1
- package/cjs/model/register-model.js +0 -94
- package/cjs/model/register-model.js.map +0 -1
- package/cjs/query-builder/query-builder.d.ts +0 -556
- package/cjs/query-builder/query-builder.d.ts.map +0 -1
- package/cjs/query-builder/query-builder.js +0 -1070
- package/cjs/query-builder/query-builder.js.map +0 -1
- package/cjs/relations/helpers.d.ts +0 -156
- package/cjs/relations/helpers.d.ts.map +0 -1
- package/cjs/relations/helpers.js +0 -202
- package/cjs/relations/helpers.js.map +0 -1
- package/cjs/relations/index.d.ts +0 -35
- package/cjs/relations/index.d.ts.map +0 -1
- package/cjs/relations/pivot-operations.d.ts +0 -160
- package/cjs/relations/pivot-operations.d.ts.map +0 -1
- package/cjs/relations/pivot-operations.js +0 -293
- package/cjs/relations/pivot-operations.js.map +0 -1
- package/cjs/relations/relation-hydrator.d.ts +0 -68
- package/cjs/relations/relation-hydrator.d.ts.map +0 -1
- package/cjs/relations/relation-hydrator.js +0 -81
- package/cjs/relations/relation-hydrator.js.map +0 -1
- package/cjs/relations/relation-loader.d.ts +0 -194
- package/cjs/relations/relation-loader.d.ts.map +0 -1
- package/cjs/relations/relation-loader.js +0 -466
- package/cjs/relations/relation-loader.js.map +0 -1
- package/cjs/relations/types.d.ts +0 -306
- package/cjs/relations/types.d.ts.map +0 -1
- package/cjs/remover/database-remover.d.ts +0 -100
- package/cjs/remover/database-remover.d.ts.map +0 -1
- package/cjs/remover/database-remover.js +0 -214
- package/cjs/remover/database-remover.js.map +0 -1
- package/cjs/restorer/database-restorer.d.ts +0 -131
- package/cjs/restorer/database-restorer.d.ts.map +0 -1
- package/cjs/restorer/database-restorer.js +0 -434
- package/cjs/restorer/database-restorer.js.map +0 -1
- package/cjs/sql-database-dirty-tracker.d.ts +0 -13
- package/cjs/sql-database-dirty-tracker.d.ts.map +0 -1
- package/cjs/sql-database-dirty-tracker.js +0 -14
- package/cjs/sql-database-dirty-tracker.js.map +0 -1
- package/cjs/sync/index.d.ts +0 -12
- package/cjs/sync/index.d.ts.map +0 -1
- package/cjs/sync/model-events.d.ts +0 -62
- package/cjs/sync/model-events.d.ts.map +0 -1
- package/cjs/sync/model-events.js +0 -49
- package/cjs/sync/model-events.js.map +0 -1
- package/cjs/sync/model-sync-operation.d.ts +0 -163
- package/cjs/sync/model-sync-operation.d.ts.map +0 -1
- package/cjs/sync/model-sync-operation.js +0 -292
- package/cjs/sync/model-sync-operation.js.map +0 -1
- package/cjs/sync/model-sync.d.ts +0 -130
- package/cjs/sync/model-sync.d.ts.map +0 -1
- package/cjs/sync/model-sync.js +0 -178
- package/cjs/sync/model-sync.js.map +0 -1
- package/cjs/sync/sync-context.d.ts +0 -70
- package/cjs/sync/sync-context.d.ts.map +0 -1
- package/cjs/sync/sync-context.js +0 -101
- package/cjs/sync/sync-context.js.map +0 -1
- package/cjs/sync/sync-manager.d.ts +0 -213
- package/cjs/sync/sync-manager.d.ts.map +0 -1
- package/cjs/sync/sync-manager.js +0 -689
- package/cjs/sync/sync-manager.js.map +0 -1
- package/cjs/sync/types.d.ts +0 -289
- package/cjs/sync/types.d.ts.map +0 -1
- package/cjs/test-migrations/test-enhanced-features.migration.d.ts +0 -15
- package/cjs/test-migrations/test-enhanced-features.migration.d.ts.map +0 -1
- package/cjs/types.d.ts +0 -371
- package/cjs/types.d.ts.map +0 -1
- package/cjs/utils/connect-to-database.d.ts +0 -307
- package/cjs/utils/connect-to-database.d.ts.map +0 -1
- package/cjs/utils/connect-to-database.js +0 -130
- package/cjs/utils/connect-to-database.js.map +0 -1
- package/cjs/utils/database-writer.utils.d.ts +0 -15
- package/cjs/utils/database-writer.utils.d.ts.map +0 -1
- package/cjs/utils/database-writer.utils.js +0 -14
- package/cjs/utils/database-writer.utils.js.map +0 -1
- package/cjs/utils/define-model.js +0 -100
- package/cjs/utils/define-model.js.map +0 -1
- package/cjs/utils/is-valid-date-value.d.ts +0 -5
- package/cjs/utils/is-valid-date-value.d.ts.map +0 -1
- package/cjs/utils/is-valid-date-value.js +0 -25
- package/cjs/utils/is-valid-date-value.js.map +0 -1
- package/cjs/utils/once-connected.d.ts +0 -146
- package/cjs/utils/once-connected.d.ts.map +0 -1
- package/cjs/utils/once-connected.js +0 -251
- package/cjs/utils/once-connected.js.map +0 -1
- package/cjs/validation/database-seal-plugins.d.ts +0 -12
- package/cjs/validation/database-seal-plugins.d.ts.map +0 -1
- package/cjs/validation/database-seal-plugins.js +0 -1
- package/cjs/validation/database-seal-plugins.js.map +0 -1
- package/cjs/validation/database-writer-validation-error.d.ts +0 -97
- package/cjs/validation/database-writer-validation-error.d.ts.map +0 -1
- package/cjs/validation/database-writer-validation-error.js +0 -160
- package/cjs/validation/database-writer-validation-error.js.map +0 -1
- package/cjs/validation/index.d.ts +0 -3
- package/cjs/validation/index.d.ts.map +0 -1
- package/cjs/validation/mutators/embed-mutator.d.ts +0 -9
- package/cjs/validation/mutators/embed-mutator.d.ts.map +0 -1
- package/cjs/validation/mutators/embed-mutator.js +0 -33
- package/cjs/validation/mutators/embed-mutator.js.map +0 -1
- package/cjs/validation/plugins/embed-validator-plugin.d.ts +0 -24
- package/cjs/validation/plugins/embed-validator-plugin.d.ts.map +0 -1
- package/cjs/validation/plugins/embed-validator-plugin.js +0 -18
- package/cjs/validation/plugins/embed-validator-plugin.js.map +0 -1
- package/cjs/validation/rules/database-model-rule.d.ts +0 -7
- package/cjs/validation/rules/database-model-rule.d.ts.map +0 -1
- package/cjs/validation/rules/database-model-rule.js +0 -27
- package/cjs/validation/rules/database-model-rule.js.map +0 -1
- package/cjs/validation/transformers/embed-model-transformer.d.ts +0 -3
- package/cjs/validation/transformers/embed-model-transformer.d.ts.map +0 -1
- package/cjs/validation/transformers/embed-model-transformer.js +0 -18
- package/cjs/validation/transformers/embed-model-transformer.js.map +0 -1
- package/cjs/validation/validators/embed-validator.d.ts +0 -21
- package/cjs/validation/validators/embed-validator.d.ts.map +0 -1
- package/cjs/validation/validators/embed-validator.js +0 -43
- package/cjs/validation/validators/embed-validator.js.map +0 -1
- package/cjs/writer/database-writer.d.ts +0 -174
- package/cjs/writer/database-writer.d.ts.map +0 -1
- package/cjs/writer/database-writer.js +0 -400
- package/cjs/writer/database-writer.js.map +0 -1
- package/esm/context/database-data-source-context.d.ts +0 -29
- package/esm/context/database-data-source-context.d.ts.map +0 -1
- package/esm/context/database-data-source-context.js +0 -28
- package/esm/context/database-data-source-context.js.map +0 -1
- package/esm/context/database-transaction-context.d.ts +0 -35
- package/esm/context/database-transaction-context.d.ts.map +0 -1
- package/esm/context/database-transaction-context.js +0 -40
- package/esm/context/database-transaction-context.js.map +0 -1
- package/esm/contracts/database-driver.contract.d.ts +0 -450
- package/esm/contracts/database-driver.contract.d.ts.map +0 -1
- package/esm/contracts/database-id-generator.contract.d.ts +0 -109
- package/esm/contracts/database-id-generator.contract.d.ts.map +0 -1
- package/esm/contracts/database-remover.contract.d.ts +0 -104
- package/esm/contracts/database-remover.contract.d.ts.map +0 -1
- package/esm/contracts/database-restorer.contract.d.ts +0 -143
- package/esm/contracts/database-restorer.contract.d.ts.map +0 -1
- package/esm/contracts/database-writer.contract.d.ts +0 -119
- package/esm/contracts/database-writer.contract.d.ts.map +0 -1
- package/esm/contracts/driver-blueprint.contract.d.ts +0 -49
- package/esm/contracts/driver-blueprint.contract.d.ts.map +0 -1
- package/esm/contracts/index.d.ts +0 -10
- package/esm/contracts/index.d.ts.map +0 -1
- package/esm/contracts/migration-driver.contract.d.ts +0 -522
- package/esm/contracts/migration-driver.contract.d.ts.map +0 -1
- package/esm/contracts/query-builder.contract.d.ts +0 -1609
- package/esm/contracts/query-builder.contract.d.ts.map +0 -1
- package/esm/contracts/sync-adapter.contract.d.ts +0 -58
- package/esm/contracts/sync-adapter.contract.d.ts.map +0 -1
- package/esm/data-source/data-source-registry.d.ts +0 -108
- package/esm/data-source/data-source-registry.d.ts.map +0 -1
- package/esm/data-source/data-source-registry.js +0 -145
- package/esm/data-source/data-source-registry.js.map +0 -1
- package/esm/data-source/data-source.d.ts +0 -147
- package/esm/data-source/data-source.d.ts.map +0 -1
- package/esm/data-source/data-source.js +0 -83
- package/esm/data-source/data-source.js.map +0 -1
- package/esm/database-dirty-tracker.d.ts +0 -252
- package/esm/database-dirty-tracker.d.ts.map +0 -1
- package/esm/database-dirty-tracker.js +0 -386
- package/esm/database-dirty-tracker.js.map +0 -1
- package/esm/drivers/mongodb/mongodb-blueprint.d.ts +0 -30
- package/esm/drivers/mongodb/mongodb-blueprint.d.ts.map +0 -1
- package/esm/drivers/mongodb/mongodb-blueprint.js +0 -51
- package/esm/drivers/mongodb/mongodb-blueprint.js.map +0 -1
- package/esm/drivers/mongodb/mongodb-driver.d.ts +0 -325
- package/esm/drivers/mongodb/mongodb-driver.d.ts.map +0 -1
- package/esm/drivers/mongodb/mongodb-driver.js +0 -845
- package/esm/drivers/mongodb/mongodb-driver.js.map +0 -1
- package/esm/drivers/mongodb/mongodb-id-generator.d.ts +0 -116
- package/esm/drivers/mongodb/mongodb-id-generator.d.ts.map +0 -1
- package/esm/drivers/mongodb/mongodb-id-generator.js +0 -149
- package/esm/drivers/mongodb/mongodb-id-generator.js.map +0 -1
- package/esm/drivers/mongodb/mongodb-migration-driver.d.ts +0 -317
- package/esm/drivers/mongodb/mongodb-migration-driver.d.ts.map +0 -1
- package/esm/drivers/mongodb/mongodb-migration-driver.js +0 -666
- package/esm/drivers/mongodb/mongodb-migration-driver.js.map +0 -1
- package/esm/drivers/mongodb/mongodb-query-builder.d.ts +0 -1122
- package/esm/drivers/mongodb/mongodb-query-builder.d.ts.map +0 -1
- package/esm/drivers/mongodb/mongodb-query-builder.js +0 -1988
- package/esm/drivers/mongodb/mongodb-query-builder.js.map +0 -1
- package/esm/drivers/mongodb/mongodb-query-operations.d.ts +0 -226
- package/esm/drivers/mongodb/mongodb-query-operations.d.ts.map +0 -1
- package/esm/drivers/mongodb/mongodb-query-operations.js +0 -270
- package/esm/drivers/mongodb/mongodb-query-operations.js.map +0 -1
- package/esm/drivers/mongodb/mongodb-query-parser.d.ts +0 -262
- package/esm/drivers/mongodb/mongodb-query-parser.d.ts.map +0 -1
- package/esm/drivers/mongodb/mongodb-query-parser.js +0 -1351
- package/esm/drivers/mongodb/mongodb-query-parser.js.map +0 -1
- package/esm/drivers/mongodb/mongodb-sync-adapter.d.ts +0 -79
- package/esm/drivers/mongodb/mongodb-sync-adapter.d.ts.map +0 -1
- package/esm/drivers/mongodb/mongodb-sync-adapter.js +0 -146
- package/esm/drivers/mongodb/mongodb-sync-adapter.js.map +0 -1
- package/esm/drivers/mongodb/types.d.ts +0 -43
- package/esm/drivers/mongodb/types.d.ts.map +0 -1
- package/esm/drivers/postgres/index.d.ts +0 -16
- package/esm/drivers/postgres/index.d.ts.map +0 -1
- package/esm/drivers/postgres/postgres-blueprint.d.ts +0 -64
- package/esm/drivers/postgres/postgres-blueprint.d.ts.map +0 -1
- package/esm/drivers/postgres/postgres-blueprint.js +0 -121
- package/esm/drivers/postgres/postgres-blueprint.js.map +0 -1
- package/esm/drivers/postgres/postgres-dialect.d.ts +0 -136
- package/esm/drivers/postgres/postgres-dialect.d.ts.map +0 -1
- package/esm/drivers/postgres/postgres-dialect.js +0 -268
- package/esm/drivers/postgres/postgres-dialect.js.map +0 -1
- package/esm/drivers/postgres/postgres-driver.d.ts +0 -432
- package/esm/drivers/postgres/postgres-driver.d.ts.map +0 -1
- package/esm/drivers/postgres/postgres-driver.js +0 -1008
- package/esm/drivers/postgres/postgres-driver.js.map +0 -1
- package/esm/drivers/postgres/postgres-migration-driver.d.ts +0 -397
- package/esm/drivers/postgres/postgres-migration-driver.d.ts.map +0 -1
- package/esm/drivers/postgres/postgres-migration-driver.js +0 -900
- package/esm/drivers/postgres/postgres-migration-driver.js.map +0 -1
- package/esm/drivers/postgres/postgres-query-builder.d.ts +0 -254
- package/esm/drivers/postgres/postgres-query-builder.d.ts.map +0 -1
- package/esm/drivers/postgres/postgres-query-builder.js +0 -933
- package/esm/drivers/postgres/postgres-query-builder.js.map +0 -1
- package/esm/drivers/postgres/postgres-query-parser.d.ts +0 -328
- package/esm/drivers/postgres/postgres-query-parser.d.ts.map +0 -1
- package/esm/drivers/postgres/postgres-query-parser.js +0 -868
- package/esm/drivers/postgres/postgres-query-parser.js.map +0 -1
- package/esm/drivers/postgres/postgres-sql-serializer.d.ts +0 -37
- package/esm/drivers/postgres/postgres-sql-serializer.d.ts.map +0 -1
- package/esm/drivers/postgres/postgres-sql-serializer.js +0 -400
- package/esm/drivers/postgres/postgres-sql-serializer.js.map +0 -1
- package/esm/drivers/postgres/postgres-sync-adapter.d.ts +0 -83
- package/esm/drivers/postgres/postgres-sync-adapter.d.ts.map +0 -1
- package/esm/drivers/postgres/postgres-sync-adapter.js +0 -204
- package/esm/drivers/postgres/postgres-sync-adapter.js.map +0 -1
- package/esm/drivers/postgres/types.d.ts +0 -144
- package/esm/drivers/postgres/types.d.ts.map +0 -1
- package/esm/drivers/sql/index.d.ts +0 -10
- package/esm/drivers/sql/index.d.ts.map +0 -1
- package/esm/drivers/sql/sql-dialect.contract.d.ts +0 -204
- package/esm/drivers/sql/sql-dialect.contract.d.ts.map +0 -1
- package/esm/drivers/sql/sql-types.d.ts +0 -202
- package/esm/drivers/sql/sql-types.d.ts.map +0 -1
- package/esm/errors/missing-data-source.error.d.ts +0 -22
- package/esm/errors/missing-data-source.error.d.ts.map +0 -1
- package/esm/errors/missing-data-source.error.js +0 -29
- package/esm/errors/missing-data-source.error.js.map +0 -1
- package/esm/errors/transaction-rollback.error.d.ts +0 -20
- package/esm/errors/transaction-rollback.error.d.ts.map +0 -1
- package/esm/errors/transaction-rollback.error.js +0 -27
- package/esm/errors/transaction-rollback.error.js.map +0 -1
- package/esm/events/model-events.d.ts +0 -231
- package/esm/events/model-events.d.ts.map +0 -1
- package/esm/events/model-events.js +0 -259
- package/esm/events/model-events.js.map +0 -1
- package/esm/expressions/aggregate-expressions.d.ts +0 -215
- package/esm/expressions/aggregate-expressions.d.ts.map +0 -1
- package/esm/expressions/aggregate-expressions.js +0 -221
- package/esm/expressions/aggregate-expressions.js.map +0 -1
- package/esm/expressions/index.d.ts +0 -2
- package/esm/expressions/index.d.ts.map +0 -1
- package/esm/index.d.ts +0 -45
- package/esm/index.d.ts.map +0 -1
- package/esm/index.js +0 -1
- package/esm/index.js.map +0 -1
- package/esm/migration/column-builder.d.ts +0 -417
- package/esm/migration/column-builder.d.ts.map +0 -1
- package/esm/migration/column-builder.js +0 -586
- package/esm/migration/column-builder.js.map +0 -1
- package/esm/migration/column-helpers.d.ts +0 -275
- package/esm/migration/column-helpers.d.ts.map +0 -1
- package/esm/migration/column-helpers.js +0 -389
- package/esm/migration/column-helpers.js.map +0 -1
- package/esm/migration/foreign-key-builder.d.ts +0 -103
- package/esm/migration/foreign-key-builder.d.ts.map +0 -1
- package/esm/migration/foreign-key-builder.js +0 -121
- package/esm/migration/foreign-key-builder.js.map +0 -1
- package/esm/migration/index.d.ts +0 -7
- package/esm/migration/index.d.ts.map +0 -1
- package/esm/migration/migration-runner.d.ts +0 -278
- package/esm/migration/migration-runner.d.ts.map +0 -1
- package/esm/migration/migration-runner.js +0 -815
- package/esm/migration/migration-runner.js.map +0 -1
- package/esm/migration/migration.d.ts +0 -1988
- package/esm/migration/migration.d.ts.map +0 -1
- package/esm/migration/migration.js +0 -2162
- package/esm/migration/migration.js.map +0 -1
- package/esm/migration/sql-grammar.d.ts +0 -61
- package/esm/migration/sql-grammar.d.ts.map +0 -1
- package/esm/migration/sql-grammar.js +0 -164
- package/esm/migration/sql-grammar.js.map +0 -1
- package/esm/migration/sql-serializer.d.ts +0 -22
- package/esm/migration/sql-serializer.d.ts.map +0 -1
- package/esm/migration/sql-serializer.js +0 -26
- package/esm/migration/sql-serializer.js.map +0 -1
- package/esm/migration/types.d.ts +0 -155
- package/esm/migration/types.d.ts.map +0 -1
- package/esm/model/methods/accessor-methods.d.ts +0 -13
- package/esm/model/methods/accessor-methods.d.ts.map +0 -1
- package/esm/model/methods/accessor-methods.js +0 -51
- package/esm/model/methods/accessor-methods.js.map +0 -1
- package/esm/model/methods/delete-methods.d.ts +0 -10
- package/esm/model/methods/delete-methods.d.ts.map +0 -1
- package/esm/model/methods/delete-methods.js +0 -10
- package/esm/model/methods/delete-methods.js.map +0 -1
- package/esm/model/methods/dirty-methods.d.ts +0 -10
- package/esm/model/methods/dirty-methods.d.ts.map +0 -1
- package/esm/model/methods/dirty-methods.js +0 -15
- package/esm/model/methods/dirty-methods.js.map +0 -1
- package/esm/model/methods/hydration-methods.d.ts +0 -10
- package/esm/model/methods/hydration-methods.d.ts.map +0 -1
- package/esm/model/methods/hydration-methods.js +0 -57
- package/esm/model/methods/hydration-methods.js.map +0 -1
- package/esm/model/methods/instance-event-methods.d.ts +0 -7
- package/esm/model/methods/instance-event-methods.d.ts.map +0 -1
- package/esm/model/methods/instance-event-methods.js +0 -15
- package/esm/model/methods/instance-event-methods.js.map +0 -1
- package/esm/model/methods/meta-methods.d.ts +0 -7
- package/esm/model/methods/meta-methods.d.ts.map +0 -1
- package/esm/model/methods/meta-methods.js +0 -78
- package/esm/model/methods/meta-methods.js.map +0 -1
- package/esm/model/methods/query-methods.d.ts +0 -24
- package/esm/model/methods/query-methods.d.ts.map +0 -1
- package/esm/model/methods/query-methods.js +0 -164
- package/esm/model/methods/query-methods.js.map +0 -1
- package/esm/model/methods/restore-methods.d.ts +0 -10
- package/esm/model/methods/restore-methods.d.ts.map +0 -1
- package/esm/model/methods/restore-methods.js +0 -13
- package/esm/model/methods/restore-methods.js.map +0 -1
- package/esm/model/methods/scope-methods.d.ts +0 -7
- package/esm/model/methods/scope-methods.d.ts.map +0 -1
- package/esm/model/methods/scope-methods.js +0 -15
- package/esm/model/methods/scope-methods.js.map +0 -1
- package/esm/model/methods/serialization-methods.d.ts +0 -3
- package/esm/model/methods/serialization-methods.d.ts.map +0 -1
- package/esm/model/methods/serialization-methods.js +0 -27
- package/esm/model/methods/serialization-methods.js.map +0 -1
- package/esm/model/methods/static-event-methods.d.ts +0 -9
- package/esm/model/methods/static-event-methods.d.ts.map +0 -1
- package/esm/model/methods/static-event-methods.js +0 -29
- package/esm/model/methods/static-event-methods.js.map +0 -1
- package/esm/model/methods/write-methods.d.ts +0 -10
- package/esm/model/methods/write-methods.d.ts.map +0 -1
- package/esm/model/methods/write-methods.js +0 -52
- package/esm/model/methods/write-methods.js.map +0 -1
- package/esm/model/model.d.ts +0 -1647
- package/esm/model/model.d.ts.map +0 -1
- package/esm/model/model.js +0 -1657
- package/esm/model/model.js.map +0 -1
- package/esm/model/model.types.d.ts +0 -44
- package/esm/model/model.types.d.ts.map +0 -1
- package/esm/model/register-model.d.ts +0 -81
- package/esm/model/register-model.d.ts.map +0 -1
- package/esm/model/register-model.js +0 -94
- package/esm/model/register-model.js.map +0 -1
- package/esm/query-builder/query-builder.d.ts +0 -556
- package/esm/query-builder/query-builder.d.ts.map +0 -1
- package/esm/query-builder/query-builder.js +0 -1070
- package/esm/query-builder/query-builder.js.map +0 -1
- package/esm/relations/helpers.d.ts +0 -156
- package/esm/relations/helpers.d.ts.map +0 -1
- package/esm/relations/helpers.js +0 -202
- package/esm/relations/helpers.js.map +0 -1
- package/esm/relations/index.d.ts +0 -35
- package/esm/relations/index.d.ts.map +0 -1
- package/esm/relations/pivot-operations.d.ts +0 -160
- package/esm/relations/pivot-operations.d.ts.map +0 -1
- package/esm/relations/pivot-operations.js +0 -293
- package/esm/relations/pivot-operations.js.map +0 -1
- package/esm/relations/relation-hydrator.d.ts +0 -68
- package/esm/relations/relation-hydrator.d.ts.map +0 -1
- package/esm/relations/relation-hydrator.js +0 -81
- package/esm/relations/relation-hydrator.js.map +0 -1
- package/esm/relations/relation-loader.d.ts +0 -194
- package/esm/relations/relation-loader.d.ts.map +0 -1
- package/esm/relations/relation-loader.js +0 -466
- package/esm/relations/relation-loader.js.map +0 -1
- package/esm/relations/types.d.ts +0 -306
- package/esm/relations/types.d.ts.map +0 -1
- package/esm/remover/database-remover.d.ts +0 -100
- package/esm/remover/database-remover.d.ts.map +0 -1
- package/esm/remover/database-remover.js +0 -214
- package/esm/remover/database-remover.js.map +0 -1
- package/esm/restorer/database-restorer.d.ts +0 -131
- package/esm/restorer/database-restorer.d.ts.map +0 -1
- package/esm/restorer/database-restorer.js +0 -434
- package/esm/restorer/database-restorer.js.map +0 -1
- package/esm/sql-database-dirty-tracker.d.ts +0 -13
- package/esm/sql-database-dirty-tracker.d.ts.map +0 -1
- package/esm/sql-database-dirty-tracker.js +0 -14
- package/esm/sql-database-dirty-tracker.js.map +0 -1
- package/esm/sync/index.d.ts +0 -12
- package/esm/sync/index.d.ts.map +0 -1
- package/esm/sync/model-events.d.ts +0 -62
- package/esm/sync/model-events.d.ts.map +0 -1
- package/esm/sync/model-events.js +0 -49
- package/esm/sync/model-events.js.map +0 -1
- package/esm/sync/model-sync-operation.d.ts +0 -163
- package/esm/sync/model-sync-operation.d.ts.map +0 -1
- package/esm/sync/model-sync-operation.js +0 -292
- package/esm/sync/model-sync-operation.js.map +0 -1
- package/esm/sync/model-sync.d.ts +0 -130
- package/esm/sync/model-sync.d.ts.map +0 -1
- package/esm/sync/model-sync.js +0 -178
- package/esm/sync/model-sync.js.map +0 -1
- package/esm/sync/sync-context.d.ts +0 -70
- package/esm/sync/sync-context.d.ts.map +0 -1
- package/esm/sync/sync-context.js +0 -101
- package/esm/sync/sync-context.js.map +0 -1
- package/esm/sync/sync-manager.d.ts +0 -213
- package/esm/sync/sync-manager.d.ts.map +0 -1
- package/esm/sync/sync-manager.js +0 -689
- package/esm/sync/sync-manager.js.map +0 -1
- package/esm/sync/types.d.ts +0 -289
- package/esm/sync/types.d.ts.map +0 -1
- package/esm/test-migrations/test-enhanced-features.migration.d.ts +0 -15
- package/esm/test-migrations/test-enhanced-features.migration.d.ts.map +0 -1
- package/esm/types.d.ts +0 -371
- package/esm/types.d.ts.map +0 -1
- package/esm/utils/connect-to-database.d.ts +0 -307
- package/esm/utils/connect-to-database.d.ts.map +0 -1
- package/esm/utils/connect-to-database.js +0 -130
- package/esm/utils/connect-to-database.js.map +0 -1
- package/esm/utils/database-writer.utils.d.ts +0 -15
- package/esm/utils/database-writer.utils.d.ts.map +0 -1
- package/esm/utils/database-writer.utils.js +0 -14
- package/esm/utils/database-writer.utils.js.map +0 -1
- package/esm/utils/define-model.js +0 -100
- package/esm/utils/define-model.js.map +0 -1
- package/esm/utils/is-valid-date-value.d.ts +0 -5
- package/esm/utils/is-valid-date-value.d.ts.map +0 -1
- package/esm/utils/is-valid-date-value.js +0 -25
- package/esm/utils/is-valid-date-value.js.map +0 -1
- package/esm/utils/once-connected.d.ts +0 -146
- package/esm/utils/once-connected.d.ts.map +0 -1
- package/esm/utils/once-connected.js +0 -251
- package/esm/utils/once-connected.js.map +0 -1
- package/esm/validation/database-seal-plugins.d.ts +0 -12
- package/esm/validation/database-seal-plugins.d.ts.map +0 -1
- package/esm/validation/database-seal-plugins.js +0 -1
- package/esm/validation/database-seal-plugins.js.map +0 -1
- package/esm/validation/database-writer-validation-error.d.ts +0 -97
- package/esm/validation/database-writer-validation-error.d.ts.map +0 -1
- package/esm/validation/database-writer-validation-error.js +0 -160
- package/esm/validation/database-writer-validation-error.js.map +0 -1
- package/esm/validation/index.d.ts +0 -3
- package/esm/validation/index.d.ts.map +0 -1
- package/esm/validation/mutators/embed-mutator.d.ts +0 -9
- package/esm/validation/mutators/embed-mutator.d.ts.map +0 -1
- package/esm/validation/mutators/embed-mutator.js +0 -33
- package/esm/validation/mutators/embed-mutator.js.map +0 -1
- package/esm/validation/plugins/embed-validator-plugin.d.ts +0 -24
- package/esm/validation/plugins/embed-validator-plugin.d.ts.map +0 -1
- package/esm/validation/plugins/embed-validator-plugin.js +0 -18
- package/esm/validation/plugins/embed-validator-plugin.js.map +0 -1
- package/esm/validation/rules/database-model-rule.d.ts +0 -7
- package/esm/validation/rules/database-model-rule.d.ts.map +0 -1
- package/esm/validation/rules/database-model-rule.js +0 -27
- package/esm/validation/rules/database-model-rule.js.map +0 -1
- package/esm/validation/transformers/embed-model-transformer.d.ts +0 -3
- package/esm/validation/transformers/embed-model-transformer.d.ts.map +0 -1
- package/esm/validation/transformers/embed-model-transformer.js +0 -18
- package/esm/validation/transformers/embed-model-transformer.js.map +0 -1
- package/esm/validation/validators/embed-validator.d.ts +0 -21
- package/esm/validation/validators/embed-validator.d.ts.map +0 -1
- package/esm/validation/validators/embed-validator.js +0 -43
- package/esm/validation/validators/embed-validator.js.map +0 -1
- package/esm/writer/database-writer.d.ts +0 -174
- package/esm/writer/database-writer.d.ts.map +0 -1
- package/esm/writer/database-writer.js +0 -400
- package/esm/writer/database-writer.js.map +0 -1
package/llms-full.txt
ADDED
|
@@ -0,0 +1,2027 @@
|
|
|
1
|
+
# Warlock Cascade — full skills
|
|
2
|
+
|
|
3
|
+
> Package: `@warlock.js/cascade`
|
|
4
|
+
|
|
5
|
+
> Generated artifact. Concatenates every SKILL.md and reference file under `@warlock.js/cascade/skills/`. Re-run `node scripts/generate-llms.mjs` after any change.
|
|
6
|
+
|
|
7
|
+
## aggregate-data `@warlock.js/cascade/aggregate-data/SKILL.md`
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
name: aggregate-data
|
|
11
|
+
description: 'Compute aggregates over a query — scalar `.count()` / `.sum(field)` / `.avg` / `.min` / `.max`, plus grouped rollups via the two-arg `.groupBy(fields, { alias: $agg.* })` with the `$agg` helpers and `.having(alias, op, value)` on computed aggregates. Triggers: `.count`, `.sum`, `.avg`, `.min`, `.max`, `.groupBy`, `.having`, `$agg`, `$agg.sum`, `$agg.count`; "monthly revenue report", "X per category", "group by status", "dashboard rollup"; typical import `import { Model, $agg } from "@warlock.js/cascade"`. Skip: row queries — `@warlock.js/cascade/query-data/SKILL.md`; cached aggregates — `@warlock.js/cache/use-cached-hof/SKILL.md`; competing tools raw SQL `GROUP BY`, `mongoose aggregate`, `prisma` `groupBy`.'
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Use aggregates and groupBy
|
|
15
|
+
|
|
16
|
+
Cascade's query builder isn't only for finding records — it crunches numbers too. The same calls run on MongoDB and Postgres.
|
|
17
|
+
|
|
18
|
+
## Scalar aggregates
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
const total = await Order.count(); // count is static on the model
|
|
22
|
+
const revenue = await Order.query().sum("total");
|
|
23
|
+
const avgTicket = await Order.query().avg("total");
|
|
24
|
+
const cheapest = await Order.query().min("total");
|
|
25
|
+
const priciest = await Order.query().max("total");
|
|
26
|
+
|
|
27
|
+
// Filtered
|
|
28
|
+
const monthRevenue = await Order.query()
|
|
29
|
+
.whereDateAfter("created_at", startOfMonth)
|
|
30
|
+
.sum("total");
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Each returns a single number (`Promise<number>`). Only `count` is a static shortcut on the model; `sum` / `avg` / `min` / `max` (and the date helpers) are query-builder methods — reach them via `Order.query()` or chain off `Order.where(...)`. Date filters take a single date: `whereDate(field, value)` (exact day, time ignored), `whereDateAfter` / `whereDateBefore` (one-sided), `whereDateBetween(field, [a, b])` (range) — there is no 3-arg `whereDate(field, op, value)`. By Cascade convention the scalar terminators return **`0` on zero rows — not `null`** — so you can use the result directly without a null guard.
|
|
34
|
+
|
|
35
|
+
## Grouped rollups — two-arg `groupBy(fields, aggregates)`
|
|
36
|
+
|
|
37
|
+
To compute aggregates *per group*, pass a second argument to `groupBy`: an object mapping output aliases to aggregate expressions. Build the expressions with the `$agg` helpers:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { $agg } from "@warlock.js/cascade";
|
|
41
|
+
|
|
42
|
+
const stats = await Order.query()
|
|
43
|
+
.groupBy("category", {
|
|
44
|
+
total: $agg.sum("amount"),
|
|
45
|
+
orders: $agg.count(),
|
|
46
|
+
avg: $agg.avg("amount"),
|
|
47
|
+
})
|
|
48
|
+
.get();
|
|
49
|
+
// each row: { category, total, orders, avg }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`fields` is a string or string array (`groupBy(["status", "country"], {...})` groups by each combination). Single-arg `groupBy("category")` / `groupBy(["a","b"])` groups **without** computing aggregates.
|
|
53
|
+
|
|
54
|
+
### `$agg` helpers — five cross-driver, four MongoDB-only
|
|
55
|
+
|
|
56
|
+
Cross-driver (identical call on MongoDB **and** Postgres):
|
|
57
|
+
|
|
58
|
+
- `$agg.count()` · `$agg.sum(field)` · `$agg.avg(field)` · `$agg.min(field)` · `$agg.max(field)`
|
|
59
|
+
|
|
60
|
+
MongoDB-only — on Postgres these **throw at the `.groupBy()` call** with an actionable message (there is no honest single-scalar `GROUP BY` equivalent):
|
|
61
|
+
|
|
62
|
+
- `$agg.distinct(field)` · `$agg.floor(field)` · `$agg.first(field)` · `$agg.last(field)`
|
|
63
|
+
|
|
64
|
+
If you need those shapes on Postgres, drop to `selectRaw` / `havingRaw` with explicit SQL (e.g. `array_agg(DISTINCT …)`, a window function).
|
|
65
|
+
|
|
66
|
+
### Driver-specific escape hatch
|
|
67
|
+
|
|
68
|
+
When `$agg.*` can't express it, pass a raw expression in the same slot:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
.groupBy("category", { total: "SUM(amount)" }) // Postgres
|
|
72
|
+
.groupBy("category", { total: { $sum: "$amount" } }) // MongoDB
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Raw strings pass through verbatim. A MongoDB operator object passed on Postgres throws ("not portable to SQL") — keep raw expressions driver-correct.
|
|
76
|
+
|
|
77
|
+
## `.having(...)` — filter groups by a computed aggregate
|
|
78
|
+
|
|
79
|
+
`.where()` filters rows *before* grouping (cheap, uses indexes). `.having()` filters *after* aggregation, by the alias you defined:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const big = await Order.query()
|
|
83
|
+
.groupBy("category", { total: $agg.sum("amount") })
|
|
84
|
+
.having("total", ">", 1000)
|
|
85
|
+
.get();
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Works identically on both drivers. (Internally Postgres can't reference a SELECT alias in `HAVING`, so Cascade substitutes the underlying expression for you.) `having` accepts the same shapes as `where`: `having("total", 1000)`, `having("total", ">", 1000)`, `having({ total: 1000, orders: 5 })`. A `having()` on a **grouped column** (not an aggregate alias) is left as a plain column filter.
|
|
89
|
+
|
|
90
|
+
## OrderBy on aggregates
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
await Order.query()
|
|
94
|
+
.groupBy("category", { revenue: $agg.sum("amount") })
|
|
95
|
+
.orderBy("revenue", "desc")
|
|
96
|
+
.get();
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The `orderBy` reference matches the alias from the aggregates object.
|
|
100
|
+
|
|
101
|
+
## Gotchas
|
|
102
|
+
|
|
103
|
+
- **`where` vs `having`.** Row filters go in `.where()` (before grouping, index-friendly); aggregate filters go in `.having()`.
|
|
104
|
+
- **Empty sets.** Scalar terminators return `0` on zero rows (above). For *grouped* reports, a group with no rows simply doesn't appear — a report over an empty range returns `[]`, not a row of zeros.
|
|
105
|
+
- **Don't `.all()` then `array.reduce`.** Always push aggregates to the database.
|
|
106
|
+
|
|
107
|
+
## See also
|
|
108
|
+
|
|
109
|
+
- [`@warlock.js/cascade/query-data/SKILL.md`](@warlock.js/cascade/query-data/SKILL.md) — the broader query vocabulary
|
|
110
|
+
- [`@warlock.js/cache/use-cached-hof/SKILL.md`](@warlock.js/cache/use-cached-hof/SKILL.md) — caching expensive aggregate results
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
## cascade-basics `@warlock.js/cascade/cascade-basics/SKILL.md`
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
name: cascade-basics
|
|
117
|
+
description: 'Start with @warlock.js/cascade ORM — model-first for MongoDB and Postgres, one schema (seal) does triple duty (type / validator / DB shape), model is the query entry point. Triggers: `Model`, `RegisterModel`, `connectToDatabase`, `Infer`, `v.object`; "which cascade skill do I need", "set up the ORM", "define my first model", "model-first ORM"; typical import `import { Model, RegisterModel } from "@warlock.js/cascade"`. Skip: schema vocabulary — `@warlock.js/seal/seal-basics/SKILL.md`; competing libs `mongoose`, `prisma`, `typeorm`, `drizzle`, `sequelize`, `mongodb` driver, `knex`.'
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
# Cascade basics
|
|
121
|
+
|
|
122
|
+
Model-first TypeScript ORM for MongoDB and Postgres. Query straight off the model — `User.where(...)`, `User.find(id)`, `User.paginate(...)`. One schema (via `@warlock.js/seal`) does triple duty: TS type via `Infer<>`, runtime validator on save, DB shape via the migration.
|
|
123
|
+
|
|
124
|
+
> This skill is the cascade **map** — read it first, then load the specific skill for the task.
|
|
125
|
+
|
|
126
|
+
## Install
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
yarn add @warlock.js/cascade @warlock.js/seal
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Foundations
|
|
133
|
+
|
|
134
|
+
The 10 things that are true in every cascade use:
|
|
135
|
+
|
|
136
|
+
1. **The model is the query entry point.** `User.where(...)`, `User.find(id)`, `User.paginate(...)`. No `db.users`, no separate client, no repository layer.
|
|
137
|
+
2. **One schema does triple duty.** `userSchema` via `v.object({...})` is your TS type (`Infer<typeof userSchema>`), your runtime validator on save, and the shape your migration writes against. Defined via [`@warlock.js/seal/seal-basics/SKILL.md`](@warlock.js/seal/seal-basics/SKILL.md).
|
|
138
|
+
3. **`@RegisterModel()` puts the model in the global registry.** Other models look it up by name for relations (`@BelongsTo("User")` or `@BelongsTo(lazy(() => User))`).
|
|
139
|
+
4. **Two drivers ship in-box: MongoDB and Postgres.** Same query API across both. Switch via config; the call sites stay identical.
|
|
140
|
+
5. **Migrations are required.** `cascade migrate` runs schema changes; the model class doesn't auto-create tables. See [`@warlock.js/cascade/write-migration/SKILL.md`](@warlock.js/cascade/write-migration/SKILL.md).
|
|
141
|
+
6. **`.create()` validates against the schema before persisting.** Defaults (`v.string().default(...)`) fire here. Validation errors throw — see [`@warlock.js/seal/handle-seal-errors/SKILL.md`](@warlock.js/seal/handle-seal-errors/SKILL.md).
|
|
142
|
+
7. **Three update idioms — pick by shape.** `.set(k, v).save()` for 1–2 fields, `.merge(data).save()` for object payloads, `.save()` after spread mutations. See [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md).
|
|
143
|
+
8. **`.destroy()` runs the configured delete strategy** (`permanent` / `soft` / `trash`). See [`@warlock.js/cascade/configure-delete-strategy/SKILL.md`](@warlock.js/cascade/configure-delete-strategy/SKILL.md).
|
|
144
|
+
9. **Lifecycle events fire on every meaningful moment** — `saving` / `saved`, `creating` / `created`, `deleting` / `deleted`. Hook on the model class. See [`@warlock.js/cascade/subscribe-to-model-events/SKILL.md`](@warlock.js/cascade/subscribe-to-model-events/SKILL.md).
|
|
145
|
+
10. **Transactions are first-class.** `transaction(async () => { ... })` wraps a unit; rollback on throw, commit on resolve. See [`@warlock.js/cascade/manage-transactions/SKILL.md`](@warlock.js/cascade/manage-transactions/SKILL.md).
|
|
146
|
+
|
|
147
|
+
## Minimal example — model, write, read
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { v, type Infer } from "@warlock.js/seal";
|
|
151
|
+
import { Model, RegisterModel } from "@warlock.js/cascade";
|
|
152
|
+
|
|
153
|
+
const userSchema = v.object({
|
|
154
|
+
name: v.string(),
|
|
155
|
+
email: v.string().email(),
|
|
156
|
+
status: v.literal("active", "inactive").default("active"),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
type UserSchema = Infer<typeof userSchema>;
|
|
160
|
+
|
|
161
|
+
@RegisterModel()
|
|
162
|
+
export class User extends Model<UserSchema> {
|
|
163
|
+
public static table = "users";
|
|
164
|
+
public static schema = userSchema;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Write
|
|
168
|
+
const user = await User.create({ name: "Ada Lovelace", email: "ada@example.com" });
|
|
169
|
+
user.id; // generated ID — direct property
|
|
170
|
+
user.get("status"); // "active" — default fired
|
|
171
|
+
|
|
172
|
+
// Read
|
|
173
|
+
const found = await User.find(user.id);
|
|
174
|
+
found?.get("email"); // "ada@example.com"
|
|
175
|
+
|
|
176
|
+
// Filter
|
|
177
|
+
const active = await User.where("status", "active").get();
|
|
178
|
+
const page = await User.paginate({ page: 1, limit: 20 });
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Pick a skill
|
|
182
|
+
|
|
183
|
+
| If the task is about… | Load |
|
|
184
|
+
| --- | --- |
|
|
185
|
+
| Defining a model class — schema, decorators, `Model<TSchema>`, accessors | [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md) |
|
|
186
|
+
| Querying — `.where`, `.find`, `.first`, `.all`, `.count`, `.exists`, ordering | [`@warlock.js/cascade/query-data/SKILL.md`](@warlock.js/cascade/query-data/SKILL.md) |
|
|
187
|
+
| Pagination — `.paginate`, `cursorPaginate`, `chunk` | [`@warlock.js/cascade/paginate-results/SKILL.md`](@warlock.js/cascade/paginate-results/SKILL.md) |
|
|
188
|
+
| Relations — `belongsTo` / `hasMany` / `belongsToMany`, eager loading | [`@warlock.js/cascade/define-relations/SKILL.md`](@warlock.js/cascade/define-relations/SKILL.md) |
|
|
189
|
+
| Migrations — `migration` definition, `up`/`down`, CLI | [`@warlock.js/cascade/write-migration/SKILL.md`](@warlock.js/cascade/write-migration/SKILL.md) |
|
|
190
|
+
| Transactions — `transaction(fn)`, rollback, isolation | [`@warlock.js/cascade/manage-transactions/SKILL.md`](@warlock.js/cascade/manage-transactions/SKILL.md) |
|
|
191
|
+
| Dirty tracking — `hasChanges`, `isDirty`, `getDirtyColumns`, `getDirtyColumnsWithValues` | [`@warlock.js/cascade/track-changes/SKILL.md`](@warlock.js/cascade/track-changes/SKILL.md) |
|
|
192
|
+
| Lifecycle hooks — `saving`, `saved`, `deleting`, `deleted` | [`@warlock.js/cascade/subscribe-to-model-events/SKILL.md`](@warlock.js/cascade/subscribe-to-model-events/SKILL.md) |
|
|
193
|
+
| Soft / hard / trash deletes + restore | [`@warlock.js/cascade/configure-delete-strategy/SKILL.md`](@warlock.js/cascade/configure-delete-strategy/SKILL.md) |
|
|
194
|
+
| Atomic ops — `Model.increase`, `decrease`, `atomic`, `Model.delete(filter)` | [`@warlock.js/cascade/perform-atomic-ops/SKILL.md`](@warlock.js/cascade/perform-atomic-ops/SKILL.md) |
|
|
195
|
+
| Aggregates — `.sum`, `.avg`, `.count`, `.groupBy`, `.having` | [`@warlock.js/cascade/aggregate-data/SKILL.md`](@warlock.js/cascade/aggregate-data/SKILL.md) |
|
|
196
|
+
| Vector search — `similarTo`, pgvector / Atlas vector index | [`@warlock.js/cascade/search-by-vector/SKILL.md`](@warlock.js/cascade/search-by-vector/SKILL.md) |
|
|
197
|
+
| Multiple databases — `connectToDatabase`, per-model `static dataSource` | [`@warlock.js/cascade/manage-data-sources/SKILL.md`](@warlock.js/cascade/manage-data-sources/SKILL.md) |
|
|
198
|
+
| CLI + Operations API — `cascade migrate`, `migrate:rollback`, programmatic | [`@warlock.js/cascade/run-cascade-cli/SKILL.md`](@warlock.js/cascade/run-cascade-cli/SKILL.md) |
|
|
199
|
+
|
|
200
|
+
## Things NOT to do
|
|
201
|
+
|
|
202
|
+
- Don't call `new User()` directly to create a record. Use `User.create({...})` — it runs validation, generates IDs, fires events.
|
|
203
|
+
- Don't `.set()` a relation slot (e.g. `user.set("contact", contactModel)`). Use `setRelation("contact", contactModel)` — relations have their own slot semantics.
|
|
204
|
+
- Don't forget `await` on writes. Without it, the mutation lives on the instance and never reaches the DB.
|
|
205
|
+
- Don't reach for `.count() > 0` to test existence. Use `.exists()` / `.notExists()` — short-circuits, doesn't hydrate.
|
|
206
|
+
- Don't return the raw model from an HTTP handler. `JSON.stringify(user)` returns the entire row; configure `static toJsonColumns` or `static resource` to shape the public output.
|
|
207
|
+
- Don't auto-run migrations from app code. They're a deploy step; run via `cascade migrate` or the Operations API.
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
## configure-delete-strategy `@warlock.js/cascade/configure-delete-strategy/SKILL.md`
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
name: configure-delete-strategy
|
|
214
|
+
description: 'Pick the delete behavior — `permanent` (hard delete), `soft` (set `deletedAt`, keep the row), `trash` (move to a separate table). Configure via `static deleteStrategy` or `.destroy({ strategy })`; restore via static `Model.restore(id)` / `Model.restoreAll()`. Triggers: `static deleteStrategy`, `.destroy`, `Model.restore`, `Model.restoreAll`, `deletedAtColumn`, `trashTable`; "soft delete users", "restore a deleted record", "GDPR hard delete"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: lifecycle events — `@warlock.js/cascade/subscribe-to-model-events/SKILL.md`; competing libs `mongoose-delete`, `typeorm softRemove`, `sequelize` paranoid.'
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
# Use delete strategies
|
|
218
|
+
|
|
219
|
+
`destroy()` does more than `DELETE FROM table`. Cascade supports three strategies that change what happens to the record. Your data source's `defaultDeleteStrategy` applies unless you override per-model or per-call; with nothing configured the fallback is `permanent`.
|
|
220
|
+
|
|
221
|
+
## The three strategies
|
|
222
|
+
|
|
223
|
+
`DeleteStrategy` is `"permanent" | "soft" | "trash"`.
|
|
224
|
+
|
|
225
|
+
| Strategy | Behavior | Reversible? |
|
|
226
|
+
| --- | --- | --- |
|
|
227
|
+
| `permanent` | DELETE the row / document (the default fallback) | No |
|
|
228
|
+
| `soft` | Set the `deletedAt` column; the row stays in the table | Yes — via `Model.restore(id)` |
|
|
229
|
+
| `trash` | Move the record to a separate table / collection, then delete the original | Yes — via `Model.restore(id)` |
|
|
230
|
+
|
|
231
|
+
## Set the default per-model
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import { Model, RegisterModel } from "@warlock.js/cascade";
|
|
235
|
+
|
|
236
|
+
@RegisterModel()
|
|
237
|
+
export class User extends Model<UserSchema> {
|
|
238
|
+
public static table = "users";
|
|
239
|
+
public static schema = userSchema;
|
|
240
|
+
public static deleteStrategy = "soft" as const; // every destroy() is soft unless overridden
|
|
241
|
+
public static deletedAtColumn = "deletedAt"; // default; set false to disable the column
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Override per-call
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
await user.destroy(); // uses the model's strategy
|
|
249
|
+
await user.destroy({ strategy: "permanent" }); // GDPR-compliant hard delete
|
|
250
|
+
await user.destroy({ strategy: "trash" }); // move to the trash table
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Resolution order: `destroy({ strategy })` → `static deleteStrategy` → data source `defaultDeleteStrategy` → `"permanent"`.
|
|
254
|
+
|
|
255
|
+
## Restoring — static, by id
|
|
256
|
+
|
|
257
|
+
Restore is a **static** operation keyed by the record's id. It auto-detects whether the record was soft-deleted or trashed:
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
const user = await User.restore(123); // clears deletedAt or pulls from trash; fires "restored"
|
|
261
|
+
const user2 = await User.restore(123, { onIdConflict: "fail" });
|
|
262
|
+
|
|
263
|
+
await User.restoreAll(); // restore every deleted record for this model
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
There is no instance `user.restore()` — call `User.restore(id)`. `restoreAll(options?)` restores all soft-deleted (or all trashed) records for the model's table.
|
|
267
|
+
|
|
268
|
+
## Trash strategy — a separate store
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
await user.destroy({ strategy: "trash" }); // moved to the model's trash table, original deleted
|
|
272
|
+
|
|
273
|
+
await User.restore(user.id); // pull back out of the trash table
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The trash table name resolves as: `static trashTable` on the model → data source `defaultTrashTable` → the `{table}Trash` pattern (e.g. `usersTrash`). Useful when soft-delete clutter would bloat the main table.
|
|
277
|
+
|
|
278
|
+
## Querying soft-deleted records — you filter, Cascade doesn't
|
|
279
|
+
|
|
280
|
+
**Important:** Cascade does **not** auto-hide soft-deleted rows. A plain `User.all()` returns deleted rows too, because the query builder has no built-in `deletedAt` filter. If you want the common "active records only" default, register a global scope yourself:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
@RegisterModel()
|
|
284
|
+
export class User extends Model<UserSchema> {
|
|
285
|
+
public static table = "users";
|
|
286
|
+
public static schema = userSchema;
|
|
287
|
+
public static deleteStrategy = "soft" as const;
|
|
288
|
+
|
|
289
|
+
static {
|
|
290
|
+
// hide soft-deleted rows from every query by default
|
|
291
|
+
this.addGlobalScope("notDeleted", (query) => {
|
|
292
|
+
query.whereNull("deletedAt");
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
With that scope in place:
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
await User.all(); // active only (scope applied)
|
|
302
|
+
await User.query().withoutGlobalScope("notDeleted").get(); // active + soft-deleted
|
|
303
|
+
await User.query().withoutGlobalScope("notDeleted").whereNotNull("deletedAt").get(); // only deleted
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
`withoutGlobalScope("notDeleted")` bypasses the filter for one query; `withoutGlobalScopes()` drops all of them. See [`@warlock.js/cascade/query-data/SKILL.md`](@warlock.js/cascade/query-data/SKILL.md) for scopes.
|
|
307
|
+
|
|
308
|
+
## Lifecycle hooks fire on all strategies
|
|
309
|
+
|
|
310
|
+
The delete events are `deleting` / `deleted` (not `destroying` / `destroyed`):
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
User.on("deleting", async (user) => {
|
|
314
|
+
/* about to delete, any strategy */
|
|
315
|
+
});
|
|
316
|
+
User.on("deleted", async (user) => {
|
|
317
|
+
/* delete completed; context carries the strategy + trashRecord for trash */
|
|
318
|
+
});
|
|
319
|
+
User.on("restored", async (user) => {
|
|
320
|
+
/* soft-delete or trash restoration completed */
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
See [`@warlock.js/cascade/subscribe-to-model-events/SKILL.md`](@warlock.js/cascade/subscribe-to-model-events/SKILL.md).
|
|
325
|
+
|
|
326
|
+
## Things NOT to do
|
|
327
|
+
|
|
328
|
+
- Don't switch a model to `soft` without a migration adding the `deletedAt` column. The strategy writes to it.
|
|
329
|
+
- Don't assume soft-deleted rows are hidden automatically — they're not. Add a `notDeleted` global scope (above) or filter `whereNull("deletedAt")` explicitly.
|
|
330
|
+
- Don't call `user.restore()` — restore is static: `User.restore(id)` / `User.restoreAll()`.
|
|
331
|
+
- Don't expect `restore` to undo a `permanent` delete. Hard deletes are gone.
|
|
332
|
+
- Don't use soft delete for GDPR right-to-be-forgotten data. The record stays in the DB; you need `permanent` (plus backup cleanup).
|
|
333
|
+
|
|
334
|
+
## See also
|
|
335
|
+
|
|
336
|
+
- [`@warlock.js/cascade/subscribe-to-model-events/SKILL.md`](@warlock.js/cascade/subscribe-to-model-events/SKILL.md) — `deleting` / `deleted` / `restored` events
|
|
337
|
+
- [`@warlock.js/cascade/query-data/SKILL.md`](@warlock.js/cascade/query-data/SKILL.md) — global scopes and `withoutGlobalScope` for surfacing deleted rows
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
## define-model `@warlock.js/cascade/define-model/SKILL.md`
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
name: define-model
|
|
344
|
+
description: 'Define a Cascade model — `@RegisterModel()`, class extends `Model<TSchema>`, `static table`, `static schema`, three update idioms (`.set` / `.merge` / `.save`), `.unset`, `.destroy`, `static toJsonColumns` / `resource` for output shaping. Triggers: `Model`, `RegisterModel`, `static schema`, `.set`, `.merge`, `.save`, `.unset`, `.destroy`, `toJsonColumns`, `resource`; "how do I define a model", "shape the JSON output", "remove a field"; typical import `import { Model, RegisterModel } from "@warlock.js/cascade"`. Skip: querying — `@warlock.js/cascade/query-data/SKILL.md`; relations — `@warlock.js/cascade/define-relations/SKILL.md`; competing libs `mongoose`, `prisma`, `typeorm` `@Entity`.'
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
# Define a model
|
|
348
|
+
|
|
349
|
+
Four moves: schema → class → write → read. Every Cascade model uses the same shape.
|
|
350
|
+
|
|
351
|
+
## Step 1 — Define the schema
|
|
352
|
+
|
|
353
|
+
The schema is your single declaration of what a record looks like. Same `v.object` does triple duty later: validates incoming data, infers the TypeScript type, and is the shape your table writes against.
|
|
354
|
+
|
|
355
|
+
```ts title="src/app/users/models/user/user.model.ts"
|
|
356
|
+
import { v, type Infer } from "@warlock.js/seal";
|
|
357
|
+
|
|
358
|
+
export const userSchema = v.object({
|
|
359
|
+
name: v.string(),
|
|
360
|
+
email: v.string().email(),
|
|
361
|
+
status: v.literal("active", "inactive").default("active"),
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
export type UserSchema = Infer<typeof userSchema>;
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
- Fields are **required by default** — chain `.optional()` on any field that may be missing.
|
|
368
|
+
- `Infer<typeof userSchema>` derives the TS type. No second declaration, no drift.
|
|
369
|
+
- The schema is standalone — reuse it for HTTP body validation, service-input validation, anywhere else.
|
|
370
|
+
- See [`@warlock.js/seal/seal-basics/SKILL.md`](@warlock.js/seal/seal-basics/SKILL.md) for the validator vocabulary.
|
|
371
|
+
|
|
372
|
+
## Step 2 — Define the model class
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
import { Model, RegisterModel } from "@warlock.js/cascade";
|
|
376
|
+
|
|
377
|
+
@RegisterModel()
|
|
378
|
+
export class User extends Model<UserSchema> {
|
|
379
|
+
public static table = "users";
|
|
380
|
+
public static schema = userSchema;
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
- `@RegisterModel()` puts `User` in the global registry. That registry is what lets relations look each other up by name.
|
|
385
|
+
- `extends Model<UserSchema>` gives the class the entire CRUD/query API typed against your schema.
|
|
386
|
+
- `static table` matches the migration. Plural, lowercase, snake_case is the convention on both drivers.
|
|
387
|
+
- `static schema` attaches the validator. On every `save()`, the data goes through `userSchema` before it hits the database.
|
|
388
|
+
|
|
389
|
+
## Read state — `.id` and `.get(field)`
|
|
390
|
+
|
|
391
|
+
```ts
|
|
392
|
+
user.id; // direct property — ID is so common cascade exposes it directly
|
|
393
|
+
user.get("status"); // canonical reader for every other column
|
|
394
|
+
user.get<number>("age"); // TypeScript generic for typed reads
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
Use the direct `.id` property; use `.get("field")` for everything else. Add a typed getter on the model class when the same field is read in many places — turns N typed-cast call sites into one named accessor.
|
|
398
|
+
|
|
399
|
+
## Write — three update idioms
|
|
400
|
+
|
|
401
|
+
Cascade gives you three ways to update an instance. Knowing when each fits saves you from reaching for the wrong one.
|
|
402
|
+
|
|
403
|
+
### `.set(field, value).save()` — one field change
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
await user.set("status", "inactive").save();
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Direct, reads top to bottom. `.set()` stages; `.save()` persists and fires events.
|
|
410
|
+
|
|
411
|
+
### `.merge(data).save()` — bulk update from an object
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
await user.merge({ name: "Augusta Ada King", status: "active" }).save();
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
The everyday case. Service takes `Partial<UserSchema>` from a request body, merges it into the instance, saves. Existing fields not in the object are untouched.
|
|
418
|
+
|
|
419
|
+
### `.save()` after manual mutation — when changes are spread
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
user.set("status", "inactive");
|
|
423
|
+
|
|
424
|
+
if (someCondition) {
|
|
425
|
+
user.set("online_state", "offline");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
await user.save();
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Quick reference:
|
|
432
|
+
|
|
433
|
+
| Situation | Pattern |
|
|
434
|
+
| --- | --- |
|
|
435
|
+
| 1–2 specific fields | `user.set(k, v).save()` |
|
|
436
|
+
| Bulk from an object | `user.merge(data).save()` |
|
|
437
|
+
| After spread mutations | `user.save()` |
|
|
438
|
+
|
|
439
|
+
## Remove a field — `.unset(field)`
|
|
440
|
+
|
|
441
|
+
```ts
|
|
442
|
+
await user.unset("image").save();
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
`.unset()` marks the field for removal — Postgres sets it to `NULL`, MongoDB drops the field entirely. Different from `.set("image", null)` which stores an explicit null (and may fail validation if the field isn't `.optional()`).
|
|
446
|
+
|
|
447
|
+
## Delete — `.destroy()`
|
|
448
|
+
|
|
449
|
+
```ts
|
|
450
|
+
await user.destroy();
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Runs the model's lifecycle (events, configured delete strategy), then removes the record. See [`@warlock.js/cascade/configure-delete-strategy/SKILL.md`](@warlock.js/cascade/configure-delete-strategy/SKILL.md) for soft / hard / trash semantics.
|
|
454
|
+
|
|
455
|
+
## Public output shaping
|
|
456
|
+
|
|
457
|
+
`JSON.stringify(user)` calls `model.toJSON()` under the hood. With no configuration, it returns the entire row. **Always configure shaping when the model is returned from an HTTP handler.**
|
|
458
|
+
|
|
459
|
+
### Fast escape — `static toJsonColumns`
|
|
460
|
+
|
|
461
|
+
```ts
|
|
462
|
+
@RegisterModel()
|
|
463
|
+
export class User extends Model<UserSchema> {
|
|
464
|
+
public static table = "users";
|
|
465
|
+
public static schema = userSchema;
|
|
466
|
+
public static toJsonColumns = ["id", "name", "email"];
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Anything outside the allow-list is dropped from serialization. Use when the public shape is a strict subset of the columns.
|
|
471
|
+
|
|
472
|
+
### Richer path — `static resource`
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
class UserResource {
|
|
476
|
+
public constructor(private data: Record<string, unknown>) {}
|
|
477
|
+
|
|
478
|
+
public toJSON() {
|
|
479
|
+
return {
|
|
480
|
+
id: this.data.id,
|
|
481
|
+
displayName: this.data.name,
|
|
482
|
+
contactEmail: this.data.email,
|
|
483
|
+
avatar: this.data.image ?? null,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
@RegisterModel()
|
|
489
|
+
export class User extends Model<UserSchema> {
|
|
490
|
+
public static table = "users";
|
|
491
|
+
public static schema = userSchema;
|
|
492
|
+
public static resource = UserResource;
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Plain TypeScript class. No framework dependencies. `static resourceColumns` narrows which columns reach the resource; pair the two for strongly-typed public output.
|
|
497
|
+
|
|
498
|
+
## Things NOT to do
|
|
499
|
+
|
|
500
|
+
- Don't `new User()` to create a record — `User.create({...})` validates, persists, fires events.
|
|
501
|
+
- Don't `.set("relation_name", instance)` for a relation slot. Use `setRelation("name", instance)`.
|
|
502
|
+
- Don't return the raw model from an HTTP route without shaping output. Add `toJsonColumns` or `resource`.
|
|
503
|
+
- Don't `await user.save()` and forget the `await` — your changes live only on the instance and never reach the DB.
|
|
504
|
+
- Don't expect schema defaults to apply on `.merge()` — defaults fire on `.create()` only.
|
|
505
|
+
|
|
506
|
+
## See also
|
|
507
|
+
|
|
508
|
+
- [`@warlock.js/cascade/query-data/SKILL.md`](@warlock.js/cascade/query-data/SKILL.md) — finding and filtering records
|
|
509
|
+
- [`@warlock.js/cascade/define-relations/SKILL.md`](@warlock.js/cascade/define-relations/SKILL.md) — relations and eager loading
|
|
510
|
+
- [`@warlock.js/cascade/track-changes/SKILL.md`](@warlock.js/cascade/track-changes/SKILL.md) — dirty tracking
|
|
511
|
+
- [`@warlock.js/cascade/subscribe-to-model-events/SKILL.md`](@warlock.js/cascade/subscribe-to-model-events/SKILL.md) — lifecycle hooks
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
## define-relations `@warlock.js/cascade/define-relations/SKILL.md`
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
name: define-relations
|
|
518
|
+
description: 'Define and query relations — `@BelongsTo` / `@HasMany` / `@BelongsToMany`, `.with("relation")` eager loading, `.whereHas(relation, cb)` filter-by-related, `setRelation` on save, `.joinWith` for SQL joins, `loadRelation`, `lazy(() => Model)`. Triggers: `@BelongsTo`, `@HasMany`, `@BelongsToMany`, `.with`, `.whereHas`, `setRelation`, `.joinWith`, `lazy`; "define a relation", "avoid N+1", "eager load posts", "filter parents by child"; typical import `import { BelongsTo, HasMany, BelongsToMany } from "@warlock.js/cascade"`. Skip: model basics — `@warlock.js/cascade/define-model/SKILL.md`; competing libs `mongoose populate`, `prisma include`, `typeorm relations`.'
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
# Define and query relations
|
|
522
|
+
|
|
523
|
+
Relations are decorators on model fields. Cascade's global registry (populated by `@RegisterModel()`) lets each relation look up its peer by name — so `@BelongsTo("User")` finds the `User` class without an import (which avoids circular-dep hell when two models reference each other).
|
|
524
|
+
|
|
525
|
+
## Three core relation types
|
|
526
|
+
|
|
527
|
+
### `@BelongsTo(target)` — the foreign-key side
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
import { BelongsTo, Model, RegisterModel } from "@warlock.js/cascade";
|
|
531
|
+
|
|
532
|
+
@RegisterModel()
|
|
533
|
+
export class Post extends Model<PostSchema> {
|
|
534
|
+
public static table = "posts";
|
|
535
|
+
public static schema = postSchema;
|
|
536
|
+
|
|
537
|
+
@BelongsTo("User", { foreignKey: "author_id" })
|
|
538
|
+
public author!: User;
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
`Post.belongs_to(User)`. The `author_id` column lives on `posts`. Pass the model name (string) for late-bound lookup, or a `lazy(() => User)` thunk if you need explicit type checking. `lazy` comes from `@mongez/reinforcements` (`import { lazy } from "@mongez/reinforcements"`), not from cascade.
|
|
543
|
+
|
|
544
|
+
### `@HasMany(target)` — the inverse, one-to-many
|
|
545
|
+
|
|
546
|
+
```ts
|
|
547
|
+
@RegisterModel()
|
|
548
|
+
export class User extends Model<UserSchema> {
|
|
549
|
+
public static table = "users";
|
|
550
|
+
public static schema = userSchema;
|
|
551
|
+
|
|
552
|
+
@HasMany("Post", { foreignKey: "author_id" })
|
|
553
|
+
public posts!: Post[];
|
|
554
|
+
}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
`User.has_many(Post)`. Same `author_id` column on `posts` — the relation is described from both ends so eager loading and filtering work in both directions.
|
|
558
|
+
|
|
559
|
+
### `@BelongsToMany(target, options)` — many-to-many via a pivot
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
@RegisterModel()
|
|
563
|
+
export class User extends Model<UserSchema> {
|
|
564
|
+
// ...
|
|
565
|
+
|
|
566
|
+
@BelongsToMany("Role", {
|
|
567
|
+
pivot: "user_roles",
|
|
568
|
+
localKey: "user_id", // pivot column → this model (User)
|
|
569
|
+
foreignKey: "role_id", // pivot column → the related model (Role)
|
|
570
|
+
})
|
|
571
|
+
public roles!: Role[];
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
`User.belongs_to_many(Role)` via the `user_roles` pivot table with `user_id` + `role_id`. The pivot's two columns are `localKey` (this model's FK) and `foreignKey` (the related model's FK) — there is no `relatedKey` option. Both sides of the many-to-many declare the relation (symmetric).
|
|
576
|
+
|
|
577
|
+
#### Pivot operations
|
|
578
|
+
|
|
579
|
+
Manage pivot rows through `model.pivot(relation)`, which returns the relation's `PivotOperations` handle:
|
|
580
|
+
|
|
581
|
+
```ts
|
|
582
|
+
await user.pivot("roles").attach([adminId, editorId]); // add (skips existing)
|
|
583
|
+
await user.pivot("roles").attach([adminId], { addedBy: actorId }); // + extra pivot columns
|
|
584
|
+
await user.pivot("roles").detach([editorId]); // remove a subset
|
|
585
|
+
await user.pivot("roles").detach(); // remove all
|
|
586
|
+
await user.pivot("roles").sync([adminId]); // replace the whole set
|
|
587
|
+
await user.pivot("roles").toggle([adminId]); // flip each id
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
`model.attach(relation, ids, pivotData?)` and `model.detach(relation, ids?)` are thin shortcuts for the two most common ops; `.sync()` / `.toggle()` live only on the `pivot(relation)` handle (or the standalone `createPivotOperations(model, relation)`). Passing a non-`belongsToMany` relation throws. Routing through `model.pivot(relation)` keeps the join-table `.sync()` distinct from `Model.sync(Target, field)` (the denormalization-embed feature). Pivot ops run direct driver writes — no model lifecycle events fire on the related model.
|
|
591
|
+
|
|
592
|
+
## Eager loading — `.with(relation)`
|
|
593
|
+
|
|
594
|
+
Load related models in the same query, avoiding N+1:
|
|
595
|
+
|
|
596
|
+
```ts
|
|
597
|
+
const posts = await Post.with("author").get();
|
|
598
|
+
|
|
599
|
+
for (const post of posts) {
|
|
600
|
+
post.getRelation("author"); // already loaded — no second query
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
Multiple relations:
|
|
605
|
+
|
|
606
|
+
```ts
|
|
607
|
+
await User.with("posts", "roles").get();
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
Nested relations via dot:
|
|
611
|
+
|
|
612
|
+
```ts
|
|
613
|
+
await User.with("posts.comments").get();
|
|
614
|
+
// Loads users → their posts → comments for each post — all in 3 queries, not N²
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
## Filtering by related conditions — `.whereHas`
|
|
618
|
+
|
|
619
|
+
Filter parents based on conditions on children:
|
|
620
|
+
|
|
621
|
+
```ts
|
|
622
|
+
const usersWithPublishedPosts = await User
|
|
623
|
+
.whereHas("posts", (query) => {
|
|
624
|
+
query.where("status", "published");
|
|
625
|
+
})
|
|
626
|
+
.get();
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
`.whereHas(relation, callback)` runs the callback on a query builder scoped to the related model. Use when "find Xs that have at least one Y matching Z."
|
|
630
|
+
|
|
631
|
+
## Setting a relation on save — `setRelation`
|
|
632
|
+
|
|
633
|
+
When you persist a model that ALSO needs to wire a relation slot at the same time, use `setRelation` — NOT `.set()`:
|
|
634
|
+
|
|
635
|
+
```ts
|
|
636
|
+
const post = new Post();
|
|
637
|
+
post.merge({ title: "Hello", body: "..." });
|
|
638
|
+
post.setRelation("author", currentUser);
|
|
639
|
+
await post.save();
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
Why not `post.set("author", currentUser)`? `set` treats the value as a column write — for a relation slot, you'd be writing the User instance into a column. `setRelation` knows it's a relation, picks the right foreign-key column, and stores the ID.
|
|
643
|
+
|
|
644
|
+
## Reading loaded relations
|
|
645
|
+
|
|
646
|
+
```ts
|
|
647
|
+
const post = await Post.with("author").first();
|
|
648
|
+
|
|
649
|
+
post.getRelation("author"); // typed access to the loaded User
|
|
650
|
+
post.author; // direct field — only if you declared it as a typed field
|
|
651
|
+
|
|
652
|
+
// Without eager loading — load on demand, then read:
|
|
653
|
+
await post.load("author");
|
|
654
|
+
const author = post.getRelation("author");
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
`getRelation("name")` returns the loaded relation (or null if not loaded). `post.load("relation", ...)` lazy-loads one or more relations onto the instance and returns the model (`this`) — read the value back with `getRelation` afterward. Useful when you didn't `.with()` upfront and need a relation conditionally.
|
|
658
|
+
|
|
659
|
+
## Joins — when eager loading isn't enough
|
|
660
|
+
|
|
661
|
+
`.with()` runs separate queries and hydrates relations. `.joinWith()` joins at the SQL/aggregation level:
|
|
662
|
+
|
|
663
|
+
```ts
|
|
664
|
+
const result = await User
|
|
665
|
+
.joinWith("posts")
|
|
666
|
+
.where("posts.status", "published")
|
|
667
|
+
.get();
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
Use `.joinWith` when you need conditions across both tables in a single query. Use `.with` when you want hydrated relation models without joining.
|
|
671
|
+
|
|
672
|
+
## Sync — embedded relations stay fresh
|
|
673
|
+
|
|
674
|
+
For embedded/denormalized relations (cached `author_name` on posts, etc.), Cascade's sync system keeps the embedded copy in step with the source-of-truth via `Model.sync(Target, field)` and the `@warlock.js/cascade` sync helpers.
|
|
675
|
+
|
|
676
|
+
## Things NOT to do
|
|
677
|
+
|
|
678
|
+
- Don't `post.set("author", user)`. Use `setRelation("author", user)`. `set` treats the value as a column write; `setRelation` picks the right foreign key.
|
|
679
|
+
- Don't load all parents and iterate to fetch each child — that's the N+1 problem. Eager-load with `.with(...)` (or load specific children with `.load(...)` after the parent fetch).
|
|
680
|
+
- Don't define a relation on only one side. Both ends of `BelongsTo`/`HasMany` and both sides of `BelongsToMany` need the decorator for queries to work bidirectionally.
|
|
681
|
+
- Don't import the related class for `@BelongsTo` if it would create a circular dependency. Use the string form (`@BelongsTo("User")`) or `lazy(() => User)`.
|
|
682
|
+
|
|
683
|
+
## See also
|
|
684
|
+
|
|
685
|
+
- [`@warlock.js/cascade/query-data/SKILL.md`](@warlock.js/cascade/query-data/SKILL.md) — `.whereHas` and other filters
|
|
686
|
+
- [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md) — `@RegisterModel`, the registry that makes name-based lookup work
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
## manage-data-sources `@warlock.js/cascade/manage-data-sources/SKILL.md`
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
name: manage-data-sources
|
|
693
|
+
description: 'Configure multiple databases — register each via `connectToDatabase({ name, driver, database, isDefault })`, assign a model with `static dataSource = "name"`, route a migration with `dataSource` on the migration class, inspect via `dataSourceRegistry.get(name)` / `getAllDataSources()`. The first (or `isDefault: true`) source is the default. Triggers: `connectToDatabase`, `dataSourceRegistry`, `dataSourceRegistry.get`, `getAllDataSources`, `static dataSource`; "multi-database app", "per-tenant DB", "analytics on separate DB"; typical import `import { connectToDatabase, dataSourceRegistry } from "@warlock.js/cascade"`. Skip: per-source migrations — `@warlock.js/cascade/write-migration/SKILL.md`; transaction scope — `@warlock.js/cascade/manage-transactions/SKILL.md`; competing patterns `mongoose.createConnection`, `typeorm` `DataSource`, `prisma` multi-schema.'
|
|
694
|
+
---
|
|
695
|
+
|
|
696
|
+
# Manage data sources
|
|
697
|
+
|
|
698
|
+
Most apps run against a single database, and Cascade's defaults assume that. When you need multiple — analytics on a separate DB, per-tenant DBs — register each one and bind models to the right source.
|
|
699
|
+
|
|
700
|
+
## Register sources at boot
|
|
701
|
+
|
|
702
|
+
`connectToDatabase()` builds the driver, registers the data source, and connects. Call it once per database:
|
|
703
|
+
|
|
704
|
+
```ts
|
|
705
|
+
import { connectToDatabase } from "@warlock.js/cascade";
|
|
706
|
+
|
|
707
|
+
await connectToDatabase({
|
|
708
|
+
name: "primary",
|
|
709
|
+
driver: "postgres",
|
|
710
|
+
database: "app",
|
|
711
|
+
host: "localhost",
|
|
712
|
+
port: 5432,
|
|
713
|
+
username: "app",
|
|
714
|
+
password: "secret",
|
|
715
|
+
isDefault: true, // the default source for models that don't pin one
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
await connectToDatabase({
|
|
719
|
+
name: "analytics",
|
|
720
|
+
driver: "postgres",
|
|
721
|
+
database: "analytics",
|
|
722
|
+
host: "analytics-host",
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
await connectToDatabase({
|
|
726
|
+
name: "logs",
|
|
727
|
+
driver: "mongodb",
|
|
728
|
+
database: "logs",
|
|
729
|
+
uri: "mongodb://localhost:27017",
|
|
730
|
+
});
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
The first source registered becomes the default unless another passes `isDefault: true`. (Lower-level: `dataSourceRegistry.register({ name, driver, isDefault })` if you build the driver yourself — `register` takes a `DataSourceOptions` object whose `driver` is a `DriverContract` instance, and constructs the `DataSource` for you.)
|
|
734
|
+
|
|
735
|
+
## Assign a model to a source
|
|
736
|
+
|
|
737
|
+
```ts
|
|
738
|
+
@RegisterModel()
|
|
739
|
+
export class AnalyticsEvent extends Model<AnalyticsEventSchema> {
|
|
740
|
+
public static table = "analytics_events";
|
|
741
|
+
public static schema = analyticsEventSchema;
|
|
742
|
+
public static dataSource = "analytics"; // ← goes to the analytics DB
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
@RegisterModel()
|
|
746
|
+
export class LogEntry extends Model<LogEntrySchema> {
|
|
747
|
+
public static table = "logs";
|
|
748
|
+
public static schema = logEntrySchema;
|
|
749
|
+
public static dataSource = "logs"; // ← goes to MongoDB
|
|
750
|
+
}
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
`static dataSource` takes the registered name (or a `DataSource` instance). Models without it use the default. Same query API; different storage. There is no per-query `Model.using(name)` override — the binding lives on the class.
|
|
754
|
+
|
|
755
|
+
## Migrations per data source
|
|
756
|
+
|
|
757
|
+
A migration class can pin its target source:
|
|
758
|
+
|
|
759
|
+
```ts
|
|
760
|
+
import { Migration } from "@warlock.js/cascade";
|
|
761
|
+
|
|
762
|
+
export default class CreateEventsTable extends Migration {
|
|
763
|
+
public readonly table = "analytics_events";
|
|
764
|
+
public readonly dataSource = "analytics";
|
|
765
|
+
|
|
766
|
+
public up(): void {
|
|
767
|
+
this.createTable();
|
|
768
|
+
this.id();
|
|
769
|
+
this.string("type");
|
|
770
|
+
this.timestamps();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
public down(): void {
|
|
774
|
+
this.dropTable();
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
See [`@warlock.js/cascade/write-migration/SKILL.md`](@warlock.js/cascade/write-migration/SKILL.md) for the migration shape.
|
|
780
|
+
|
|
781
|
+
## Transactions and data sources
|
|
782
|
+
|
|
783
|
+
The top-level `transaction(fn)` helper runs on the **default** source's driver — it does not take a source name. To transact against a non-default source, reach for that source's driver directly (`dataSourceRegistry.get("analytics").driver.transaction(...)`). Transactions can't span two sources; coordinate at the application level (saga / outbox) if you need that.
|
|
784
|
+
|
|
785
|
+
```ts
|
|
786
|
+
await transaction(async () => {
|
|
787
|
+
// runs on the default source
|
|
788
|
+
await Order.create({ ... });
|
|
789
|
+
await OrderItem.createMany(items);
|
|
790
|
+
});
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
## Multi-tenant per-tenant database
|
|
794
|
+
|
|
795
|
+
For strict tenant isolation, register a source per tenant once, then bind at the model layer (or resolve the model class per tenant):
|
|
796
|
+
|
|
797
|
+
```ts
|
|
798
|
+
async function ensureTenantSource(tenantId: string) {
|
|
799
|
+
try {
|
|
800
|
+
dataSourceRegistry.get(tenantId); // throws if not registered
|
|
801
|
+
} catch {
|
|
802
|
+
const config = await loadTenantConfig(tenantId);
|
|
803
|
+
await connectToDatabase({ name: tenantId, ...config, isDefault: false });
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
Register each tenant source once and reuse it — re-registering the same name re-creates the source.
|
|
809
|
+
|
|
810
|
+
## Inspection
|
|
811
|
+
|
|
812
|
+
```ts
|
|
813
|
+
dataSourceRegistry.get("analytics"); // the DataSource instance (throws if missing)
|
|
814
|
+
dataSourceRegistry.get(); // the default DataSource
|
|
815
|
+
dataSourceRegistry.getAllDataSources(); // DataSource[] — every registered source
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
There is no `has(name)` / `list()` / `setDefault()` — guard with a `try/catch` around `get(name)`, iterate `getAllDataSources()`, and set the default via `isDefault` at registration.
|
|
819
|
+
|
|
820
|
+
## Things NOT to do
|
|
821
|
+
|
|
822
|
+
- Don't call `dataSourceRegistry.register("name", config)` — `register` takes a single `DataSourceOptions` object (`{ name, driver, isDefault }`, `driver` being a built `DriverContract`); for the common case use `connectToDatabase({ name, ... })`.
|
|
823
|
+
- Don't reach for `Model.using(name)` / `setDefault` / `has` / `list` — they don't exist. Bind via `static dataSource`, inspect via `get` / `getAllDataSources`.
|
|
824
|
+
- Don't span a transaction across two data sources. Use a saga / outbox pattern.
|
|
825
|
+
- Don't write to a read replica. Most replicas reject writes; the data is overwritten on the next replication.
|
|
826
|
+
|
|
827
|
+
## See also
|
|
828
|
+
|
|
829
|
+
- [`@warlock.js/cascade/write-migration/SKILL.md`](@warlock.js/cascade/write-migration/SKILL.md) — per-source migrations
|
|
830
|
+
- [`@warlock.js/cascade/manage-transactions/SKILL.md`](@warlock.js/cascade/manage-transactions/SKILL.md) — transactions run on the default source
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
## manage-transactions `@warlock.js/cascade/manage-transactions/SKILL.md`
|
|
834
|
+
|
|
835
|
+
---
|
|
836
|
+
name: manage-transactions
|
|
837
|
+
description: 'Wrap multi-statement work in `transaction(async () => {...})` — rollback on throw, commit on resolve, optional `isolation` level (Postgres), per-`dataSource` scope. Postgres native; MongoDB requires replica set. Triggers: `transaction`, `isolation`, `SERIALIZABLE`, `READ COMMITTED`, nested savepoints; "wrap two writes atomically", "transfer balance between accounts", "rollback on error", "MongoDB replica set transactions"; typical import `import { transaction } from "@warlock.js/cascade"`. Skip: single-row atomic ops without a transaction — `@warlock.js/cascade/perform-atomic-ops/SKILL.md`; per-source scope — `@warlock.js/cascade/manage-data-sources/SKILL.md`; competing patterns `mongoose.startSession`, `pg` `BEGIN` manually, `prisma.$transaction`, `typeorm` `QueryRunner`.'
|
|
838
|
+
---
|
|
839
|
+
|
|
840
|
+
# Use transactions
|
|
841
|
+
|
|
842
|
+
A transaction is a sequence of database operations that succeed or fail as one unit. Cascade wraps the driver-level transaction with a function-shaped API — pass a callback, throw to roll back, return to commit.
|
|
843
|
+
|
|
844
|
+
## Shape
|
|
845
|
+
|
|
846
|
+
```ts
|
|
847
|
+
import { transaction } from "@warlock.js/cascade";
|
|
848
|
+
|
|
849
|
+
await transaction(async () => {
|
|
850
|
+
const account = await Account.where("id", fromId).firstOrFail();
|
|
851
|
+
const target = await Account.where("id", toId).firstOrFail();
|
|
852
|
+
|
|
853
|
+
await account.merge({ balance: account.get<number>("balance") - amount }).save();
|
|
854
|
+
await target.merge({ balance: target.get<number>("balance") + amount }).save();
|
|
855
|
+
});
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
If either `.save()` throws, both are rolled back. If the callback returns, both are committed atomically.
|
|
859
|
+
|
|
860
|
+
## Return values
|
|
861
|
+
|
|
862
|
+
The callback's resolved value becomes the transaction's resolved value:
|
|
863
|
+
|
|
864
|
+
```ts
|
|
865
|
+
const order = await transaction(async () => {
|
|
866
|
+
const created = await Order.create({...});
|
|
867
|
+
await OrderItem.createMany(items.map(item => ({ ...item, order_id: created.id })));
|
|
868
|
+
return created;
|
|
869
|
+
});
|
|
870
|
+
// order is the created Order
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
## Rollback on throw
|
|
874
|
+
|
|
875
|
+
Any thrown error inside the callback rolls back the entire transaction and re-throws to the caller:
|
|
876
|
+
|
|
877
|
+
```ts
|
|
878
|
+
try {
|
|
879
|
+
await transaction(async () => {
|
|
880
|
+
await user.save();
|
|
881
|
+
if (someInvariantFails) {
|
|
882
|
+
throw new Error("invariant violated");
|
|
883
|
+
}
|
|
884
|
+
await audit.save();
|
|
885
|
+
});
|
|
886
|
+
} catch (error) {
|
|
887
|
+
// both user and audit writes are rolled back; error propagates here
|
|
888
|
+
}
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
## MongoDB requires a replica set
|
|
892
|
+
|
|
893
|
+
MongoDB transactions only work on replica sets — a single-node `mongod` without `--replSet` will throw. For local dev, run a single-node replica set:
|
|
894
|
+
|
|
895
|
+
```bash
|
|
896
|
+
mongod --replSet rs0 --port 27017
|
|
897
|
+
# then in mongo shell:
|
|
898
|
+
rs.initiate()
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
Postgres has no such requirement — transactions work out of the box.
|
|
902
|
+
|
|
903
|
+
## Nesting
|
|
904
|
+
|
|
905
|
+
The function-shaped `transaction(fn)` is **not** nestable — calling it inside an already-open transaction is not supported. For nested scope on Postgres, drop to the manual API (`driver.beginTransaction()`) and use savepoints explicitly. For most app code, keep a single top-level `transaction(fn)` and let any inner failure abort the whole flow.
|
|
906
|
+
|
|
907
|
+
## Explicit rollback
|
|
908
|
+
|
|
909
|
+
The callback receives a transaction context; call `ctx.rollback()` to roll back without throwing:
|
|
910
|
+
|
|
911
|
+
```ts
|
|
912
|
+
await transaction(async (ctx) => {
|
|
913
|
+
await user.save();
|
|
914
|
+
if (!isValid) {
|
|
915
|
+
ctx.rollback("validation failed");
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
Throwing works too (and re-throws to the caller); `ctx.rollback()` is the non-throwing path.
|
|
921
|
+
|
|
922
|
+
## Isolation level (Postgres)
|
|
923
|
+
|
|
924
|
+
Default is the driver's default (typically `READ COMMITTED`). Request a different level via `isolationLevel`:
|
|
925
|
+
|
|
926
|
+
```ts
|
|
927
|
+
await transaction(async () => {
|
|
928
|
+
/* ... */
|
|
929
|
+
}, { isolationLevel: "SERIALIZABLE" });
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
On `SERIALIZABLE`, Postgres may abort with a serialization failure when concurrent transactions conflict — wrap in retry logic at the caller.
|
|
933
|
+
|
|
934
|
+
## Outside the transaction
|
|
935
|
+
|
|
936
|
+
Once the callback returns, the transaction is committed. Subsequent calls — including reads — see the committed state. Don't try to "share" a model instance between inside-transaction and outside contexts; reload outside if you need fresh state.
|
|
937
|
+
|
|
938
|
+
## Side effects after commit — the outbox pattern
|
|
939
|
+
|
|
940
|
+
For "side effects must only happen if the transaction succeeded" (publish to a queue, send an email, write to a search index), don't run them inside the transaction. Use the outbox pattern: write a row to an outbox table inside the transaction, dispatch from the outbox in a separate worker after commit.
|
|
941
|
+
|
|
942
|
+
## Things NOT to do
|
|
943
|
+
|
|
944
|
+
- Don't call external APIs (HTTP, queues, file writes) inside a transaction. Long-running side effects extend the lock; failures don't roll back the external call.
|
|
945
|
+
- Don't use a single-node mongod for transactions. Run a replica set even in dev.
|
|
946
|
+
- Don't `try/catch` and swallow inside a transaction — the catch defeats the rollback. If you must, re-throw after handling.
|
|
947
|
+
- Don't pass models loaded outside the transaction into it expecting fresh reads. Reload inside.
|
|
948
|
+
|
|
949
|
+
## See also
|
|
950
|
+
|
|
951
|
+
- [`@warlock.js/cascade/perform-atomic-ops/SKILL.md`](@warlock.js/cascade/perform-atomic-ops/SKILL.md) — atomic single-document ops without a full transaction
|
|
952
|
+
- [`@warlock.js/cascade/manage-data-sources/SKILL.md`](@warlock.js/cascade/manage-data-sources/SKILL.md) — transactions run on the default source
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
## paginate-results `@warlock.js/cascade/paginate-results/SKILL.md`
|
|
956
|
+
|
|
957
|
+
---
|
|
958
|
+
name: paginate-results
|
|
959
|
+
description: 'Paginate query results — `.paginate({page, limit, filter?})` for offset (returns `data` + `pagination` total/page/limit/pages), `.cursorPaginate({limit, cursor})` for very large datasets, `.chunk(size, callback)` for streaming. Triggers: `.paginate`, `.cursorPaginate`, `.chunk`, `nextCursor`, `hasMore`, `pagination.total`; "paginate the list", "infinite scroll / load more", "stream a large table", "page 2 of users"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: filter chain — `@warlock.js/cascade/query-data/SKILL.md`; eager loading on pages — `@warlock.js/cascade/define-relations/SKILL.md`; competing libs `mongoose-paginate-v2`, `prisma` cursor, `typeorm-pagination`.'
|
|
960
|
+
---
|
|
961
|
+
|
|
962
|
+
# Paginate results
|
|
963
|
+
|
|
964
|
+
Three paginations. Pick by dataset size and access pattern.
|
|
965
|
+
|
|
966
|
+
## Offset pagination — `.paginate({ page, limit })`
|
|
967
|
+
|
|
968
|
+
The everyday case for listings with page numbers:
|
|
969
|
+
|
|
970
|
+
```ts
|
|
971
|
+
const page = await User.paginate({ page: 1, limit: 20 });
|
|
972
|
+
|
|
973
|
+
page.data; // User[]
|
|
974
|
+
page.pagination; // { total, page, limit, pages }
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
`PaginationOptions` is `{ page?, limit? }` — there is no `filter` field; filter by chaining `.where()` before `.paginate()` (below).
|
|
978
|
+
|
|
979
|
+
Chain off `.where()` for filtered pagination:
|
|
980
|
+
|
|
981
|
+
```ts
|
|
982
|
+
const activePage = await User
|
|
983
|
+
.where("status", "active")
|
|
984
|
+
.paginate({ page: 2, limit: 20 });
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
**Cost characteristic.** Offset pagination scans `offset + limit` rows on every page — page 100 with limit 20 scans 2020 rows just to skip 2000. Fine for the first few pages; not great deep in the result set.
|
|
988
|
+
|
|
989
|
+
## Cursor pagination — `.cursorPaginate({ limit, cursor? })`
|
|
990
|
+
|
|
991
|
+
For very large datasets where deep pagination matters:
|
|
992
|
+
|
|
993
|
+
```ts
|
|
994
|
+
const first = await User.query().orderBy("created_at", "desc").cursorPaginate({ limit: 20 });
|
|
995
|
+
|
|
996
|
+
first.data; // User[]
|
|
997
|
+
first.pagination.nextCursor; // opaque value — pass to the next call
|
|
998
|
+
first.pagination.hasMore; // boolean
|
|
999
|
+
|
|
1000
|
+
const next = await User.query()
|
|
1001
|
+
.orderBy("created_at", "desc")
|
|
1002
|
+
.cursorPaginate({ limit: 20, cursor: first.pagination.nextCursor });
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
The cursor fields live under `pagination` (`{ hasMore, nextCursor?, hasPrev?, prevCursor? }`), not at the top level. `cursorPaginate` and `orderBy` are query-builder methods, so start the chain with `User.query()` (or any static that returns a builder, like `User.where(...)`).
|
|
1006
|
+
|
|
1007
|
+
**Cost characteristic.** Constant time per page regardless of how far in. The cursor encodes the last record's sort key — the next query is "give me records after this point," indexed.
|
|
1008
|
+
|
|
1009
|
+
**Tradeoff.** No "total page count" — cursor pagination doesn't know how many records remain. If the UI shows "Page 3 of 50," reach for `.paginate()` instead. If it shows "Load more," cursor wins.
|
|
1010
|
+
|
|
1011
|
+
## Chunked processing — `.chunk(size, callback)`
|
|
1012
|
+
|
|
1013
|
+
For backfills, exports, and "process every record" loops:
|
|
1014
|
+
|
|
1015
|
+
```ts
|
|
1016
|
+
await User.where("status", "active").chunk(500, async (users) => {
|
|
1017
|
+
for (const user of users) {
|
|
1018
|
+
await sendEmail(user);
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
`.chunk(size, fn)` streams the table 500 records at a time, calling `fn` per batch. Constant memory regardless of total row count.
|
|
1024
|
+
|
|
1025
|
+
Return `false` from the callback to stop early:
|
|
1026
|
+
|
|
1027
|
+
```ts
|
|
1028
|
+
let processed = 0;
|
|
1029
|
+
await User.query().chunk(500, async (users) => {
|
|
1030
|
+
for (const user of users) {
|
|
1031
|
+
await process(user);
|
|
1032
|
+
processed++;
|
|
1033
|
+
if (processed >= 10_000) return false;
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
`chunk` is a query-builder method — start from `User.query()` (or `User.where(...)`) before chaining it.
|
|
1039
|
+
|
|
1040
|
+
## Pagination + relations
|
|
1041
|
+
|
|
1042
|
+
Eager-load relations on a paginated page:
|
|
1043
|
+
|
|
1044
|
+
```ts
|
|
1045
|
+
const page = await Post.with("author").paginate({ page: 1, limit: 20 });
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
See [`@warlock.js/cascade/define-relations/SKILL.md`](@warlock.js/cascade/define-relations/SKILL.md).
|
|
1049
|
+
|
|
1050
|
+
## Pagination shape
|
|
1051
|
+
|
|
1052
|
+
The default offset paginator returns:
|
|
1053
|
+
|
|
1054
|
+
```ts
|
|
1055
|
+
{
|
|
1056
|
+
data: T[],
|
|
1057
|
+
pagination: {
|
|
1058
|
+
total: number, // total matching records (extra COUNT query)
|
|
1059
|
+
page: number, // current page
|
|
1060
|
+
limit: number, // page size
|
|
1061
|
+
pages: number, // total pages
|
|
1062
|
+
},
|
|
1063
|
+
}
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
The total count requires an extra query. On very large filtered tables, this can dominate page-load time — switch to cursor pagination if the total isn't user-facing.
|
|
1067
|
+
|
|
1068
|
+
## Things NOT to do
|
|
1069
|
+
|
|
1070
|
+
- Don't use offset pagination for "Load more" infinite-scroll UIs. Cursor pagination is built for it; offset re-scans on every load.
|
|
1071
|
+
- Don't fetch `.all()` and slice in memory for pagination. Always page at the query layer.
|
|
1072
|
+
- Don't omit `.orderBy()` from `.cursorPaginate()`. The cursor encodes the sort key — without one, the cursor is meaningless and ordering is driver-dependent.
|
|
1073
|
+
- Don't keep cursor strings around past their stability window — schema changes that alter the sort key can invalidate stored cursors.
|
|
1074
|
+
|
|
1075
|
+
## See also
|
|
1076
|
+
|
|
1077
|
+
- [`@warlock.js/cascade/query-data/SKILL.md`](@warlock.js/cascade/query-data/SKILL.md) — `.where`, `.orderBy`, filter chains
|
|
1078
|
+
- [`@warlock.js/cascade/define-relations/SKILL.md`](@warlock.js/cascade/define-relations/SKILL.md) — eager loading on a paginated page
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
## perform-atomic-ops `@warlock.js/cascade/perform-atomic-ops/SKILL.md`
|
|
1082
|
+
|
|
1083
|
+
---
|
|
1084
|
+
name: perform-atomic-ops
|
|
1085
|
+
description: 'Avoid races on concurrent writes — `Model.increase(filter, field, n)` / `Model.decrease` for atomic counters, `Model.atomic(filter, ops)` for arbitrary mutations (`$set` / `$inc` / `$push` / `$pull`), `Model.createMany` / `Model.findAndUpdate` / `Model.delete` for bulk. Triggers: `Model.increase`, `Model.decrease`, `Model.atomic`, `Model.createMany`, `Model.findAndUpdate`, `Model.delete`, `$inc`, `$set`; "increment counter under concurrency", "bulk insert without N+1", "atomic update without loading"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: multi-row atomicity — `@warlock.js/cascade/manage-transactions/SKILL.md`; competing patterns `mongoose findOneAndUpdate`, `pg` `UPDATE ... SET x = x + 1`.'
|
|
1086
|
+
---
|
|
1087
|
+
|
|
1088
|
+
# Use atomic operations
|
|
1089
|
+
|
|
1090
|
+
When two requests want to change the same row at the same time, you need atomicity — a guarantee that one operation completes before the other reads. For multi-document atomicity use transactions; for single-document atomic mutations these are the right tools.
|
|
1091
|
+
|
|
1092
|
+
## Counters — `Model.increase` / `Model.decrease`
|
|
1093
|
+
|
|
1094
|
+
```ts
|
|
1095
|
+
await Post.increase({ id: postId }, "views", 1);
|
|
1096
|
+
await Product.decrease({ id: productId }, "inventory", 1);
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
Signature: `Model.increase(filter, field, amount)` / `Model.decrease(filter, field, amount)` → `Promise<number>` (matched count). Atomic at the storage layer — no read-modify-write race even under high concurrency.
|
|
1100
|
+
|
|
1101
|
+
## Arbitrary atomic mutations — `Model.atomic`
|
|
1102
|
+
|
|
1103
|
+
```ts
|
|
1104
|
+
await User.atomic({ id: userId }, {
|
|
1105
|
+
$set: { last_seen: new Date() },
|
|
1106
|
+
$inc: { login_count: 1 },
|
|
1107
|
+
});
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
`Model.atomic(filter, operations)` → `Promise<number>`. Driver-flavored atomic mutation — MongoDB has `$set` / `$inc` / `$push` / `$pull`; the Postgres driver translates the equivalents. Use when you need to combine multiple field changes atomically without loading the model first.
|
|
1111
|
+
|
|
1112
|
+
## Bulk insert — `Model.createMany`
|
|
1113
|
+
|
|
1114
|
+
```ts
|
|
1115
|
+
const created = await OrderItem.createMany([
|
|
1116
|
+
{ order_id, product_id: 1, quantity: 2 },
|
|
1117
|
+
{ order_id, product_id: 2, quantity: 1 },
|
|
1118
|
+
{ order_id, product_id: 3, quantity: 5 },
|
|
1119
|
+
]);
|
|
1120
|
+
// created: OrderItem[]
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
`Model.createMany(rows)` → `Promise<TModel[]>`. Validation runs per row; wrap in a transaction if you need strict all-or-nothing semantics.
|
|
1124
|
+
|
|
1125
|
+
## Bulk update — `Model.findAndUpdate(filter, operations)`
|
|
1126
|
+
|
|
1127
|
+
```ts
|
|
1128
|
+
const updated = await User.findAndUpdate(
|
|
1129
|
+
{ status: "pending" },
|
|
1130
|
+
{ $set: { status: "active" } },
|
|
1131
|
+
);
|
|
1132
|
+
// updated: User[] — the matched-and-updated models
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
`Model.findAndUpdate(filter, operations)` takes **update operators** (`$set` / `$inc` / `$unset`), not a plain data object, and returns the updated models. For a single record there's `Model.findOneAndUpdate(filter, operations)` → `TModel | null`, and to update strictly by id, `Model.update(id, data)` → `Promise<number>`.
|
|
1136
|
+
|
|
1137
|
+
**Important.** Per-instance lifecycle `saved` events do NOT fire for each row on `findAndUpdate`. If you need `saved` per row, iterate with `.get()` and `.save()` instead — slower but event-correct.
|
|
1138
|
+
|
|
1139
|
+
## Bulk delete — `Model.delete(filter)`
|
|
1140
|
+
|
|
1141
|
+
```ts
|
|
1142
|
+
await User.delete({ status: "spam" }); // delete all matching → count
|
|
1143
|
+
await User.deleteOne({ status: "spam" }); // delete the first match → count
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
`Model.delete(filter?)` and `Model.deleteOne(filter?)` both return `Promise<number>`. These bypass the per-instance delete strategy and `deleted` events — they are raw driver deletes.
|
|
1147
|
+
|
|
1148
|
+
For per-row event-aware (and delete-strategy-aware) bulk delete, iterate:
|
|
1149
|
+
|
|
1150
|
+
```ts
|
|
1151
|
+
const targets = await User.where("status", "spam").get();
|
|
1152
|
+
for (const user of targets) {
|
|
1153
|
+
await user.destroy();
|
|
1154
|
+
}
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
## When to reach for what
|
|
1158
|
+
|
|
1159
|
+
| Task | Reach for |
|
|
1160
|
+
| --- | --- |
|
|
1161
|
+
| Increment a counter | `Model.increase(filter, field, n)` |
|
|
1162
|
+
| Atomically change multiple fields on one record | `Model.atomic(filter, ops)` |
|
|
1163
|
+
| Insert N records | `Model.createMany(rows)` |
|
|
1164
|
+
| Update many rows with operators | `Model.findAndUpdate(filter, { $set: {...} })` |
|
|
1165
|
+
| Update one record by id | `Model.update(id, data)` |
|
|
1166
|
+
| Delete many rows (raw) | `Model.delete(filter)` |
|
|
1167
|
+
| Multi-row read-modify-write | Wrap in a [transaction](@warlock.js/cascade/manage-transactions/SKILL.md) |
|
|
1168
|
+
| Need lifecycle events / delete strategy per row | `Model.where(...).get()` + iterate + `.save()` / `.destroy()` |
|
|
1169
|
+
|
|
1170
|
+
## Things NOT to do
|
|
1171
|
+
|
|
1172
|
+
- Don't `const post = await Post.find(id); post.set("views", post.get<number>("views") + 1); await post.save();` for a counter. That's a lost-update race under concurrency. Use `Post.increase(filter, "views", 1)`.
|
|
1173
|
+
- Don't reach for `insertMany` / `updateMany` / `deleteMany` — those names don't exist on the model. Use `createMany` / `findAndUpdate` / `delete`.
|
|
1174
|
+
- Don't expect `findAndUpdate` / `delete` to fire per-row `saved` / `deleted` events or honor the delete strategy. They don't. Iterate if you need that.
|
|
1175
|
+
- Don't bulk-insert a million rows in one `createMany` call — chunk it. Most drivers cap effectively at a few thousand per round-trip.
|
|
1176
|
+
|
|
1177
|
+
## See also
|
|
1178
|
+
|
|
1179
|
+
- [`@warlock.js/cascade/manage-transactions/SKILL.md`](@warlock.js/cascade/manage-transactions/SKILL.md) — multi-row atomicity
|
|
1180
|
+
- [`@warlock.js/cascade/paginate-results/SKILL.md`](@warlock.js/cascade/paginate-results/SKILL.md) — `.chunk` for bulk-processing iteration
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
## query-data `@warlock.js/cascade/query-data/SKILL.md`
|
|
1184
|
+
|
|
1185
|
+
---
|
|
1186
|
+
name: query-data
|
|
1187
|
+
description: 'Query records via the model — `.where(field, value)` / `.where(field, op, value)`, `.find(id)` / `.first` / `.all`, `.orderBy`, `.count` / `.exists`, plus `.whereIn` / `.whereBetween` / `.whereLike` / `.pluck` / `.firstOrFail` / scopes via `addScope`. Triggers: `.where`, `.find`, `.first`, `.firstOrFail`, `.all`, `.get`, `.orderBy`, `.exists`, `.whereIn`, `.whereBetween`, `addScope`; "filter by status", "find by id", "fetch active users", "check existence"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: pagination — `@warlock.js/cascade/paginate-results/SKILL.md`; aggregates — `@warlock.js/cascade/aggregate-data/SKILL.md`.'
|
|
1188
|
+
---
|
|
1189
|
+
|
|
1190
|
+
# Query data
|
|
1191
|
+
|
|
1192
|
+
The model is the query entry point. No `db.collection("users")`, no `prisma.user.findFirst()`, no repository to import — the class queries itself.
|
|
1193
|
+
|
|
1194
|
+
## Filter — `.where()`
|
|
1195
|
+
|
|
1196
|
+
### Equality — the shorthand
|
|
1197
|
+
|
|
1198
|
+
```ts
|
|
1199
|
+
const activeUsers = await User.where("status", "active").get();
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
`User.where(field, value)` returns a query builder filtered to that condition. `.get()` runs the query and returns an array of `User` instances.
|
|
1203
|
+
|
|
1204
|
+
### Operators
|
|
1205
|
+
|
|
1206
|
+
```ts
|
|
1207
|
+
const adults = await User.where("age", ">", 18).get();
|
|
1208
|
+
const recent = await User.where("created_at", ">=", lastWeek).get();
|
|
1209
|
+
const nonAdmins = await User.where("role", "!=", "admin").get();
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
3-argument form. Common operators: `=`, `!=`, `<`, `<=`, `>`, `>=`, `in`, `notIn`, `like`, `between`. Same syntax across MongoDB and Postgres.
|
|
1213
|
+
|
|
1214
|
+
### Compound conditions
|
|
1215
|
+
|
|
1216
|
+
```ts
|
|
1217
|
+
const activeAdmins = await User
|
|
1218
|
+
.where("status", "active")
|
|
1219
|
+
.where("role", "admin")
|
|
1220
|
+
.get();
|
|
1221
|
+
```
|
|
1222
|
+
|
|
1223
|
+
Chained `.where()` calls combine with `AND`.
|
|
1224
|
+
|
|
1225
|
+
### Object form
|
|
1226
|
+
|
|
1227
|
+
```ts
|
|
1228
|
+
const targets = await User.where({ status: "active", role: "admin" }).get();
|
|
1229
|
+
```
|
|
1230
|
+
|
|
1231
|
+
Equivalent to chained equalities. Useful when the filter comes from a dynamic source. **Object form only supports equality** — use chained `.where()` for operators.
|
|
1232
|
+
|
|
1233
|
+
## Get one record
|
|
1234
|
+
|
|
1235
|
+
### By ID
|
|
1236
|
+
|
|
1237
|
+
```ts
|
|
1238
|
+
const user = await User.find(id); // User | null
|
|
1239
|
+
```
|
|
1240
|
+
|
|
1241
|
+
### First match
|
|
1242
|
+
|
|
1243
|
+
```ts
|
|
1244
|
+
const anyUser = await User.first(); // first user, any
|
|
1245
|
+
const firstAdmin = await User.first({ role: "admin" }); // first admin
|
|
1246
|
+
const filtered = await User.where("status", "active").first(); // chain into .first()
|
|
1247
|
+
```
|
|
1248
|
+
|
|
1249
|
+
`.first()` with no args returns the very first record (driver-dependent default order). With a filter object, the first match by equality. Chain off `.where()` when you need operators.
|
|
1250
|
+
|
|
1251
|
+
### Throw if missing — `.firstOrFail()`
|
|
1252
|
+
|
|
1253
|
+
```ts
|
|
1254
|
+
const user = await User.where("id", req.params.id).firstOrFail();
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
Throws when nothing matches — useful when you KNOW it should exist and want the error to surface loudly instead of an `undefined`-derived NPE downstream.
|
|
1258
|
+
|
|
1259
|
+
**Always handle `null`** from `.find()` and `.first()` — use `?.` or a guard. Resist `!` on query results.
|
|
1260
|
+
|
|
1261
|
+
## Order and paginate
|
|
1262
|
+
|
|
1263
|
+
```ts
|
|
1264
|
+
const newest = await User
|
|
1265
|
+
.where("status", "active")
|
|
1266
|
+
.orderBy("created_at", "desc")
|
|
1267
|
+
.get();
|
|
1268
|
+
```
|
|
1269
|
+
|
|
1270
|
+
`.orderBy(field, "asc" | "desc")` sorts. Default direction is `"asc"`. Chain multiple `.orderBy()` for tiebreakers.
|
|
1271
|
+
|
|
1272
|
+
For pagination see [`@warlock.js/cascade/paginate-results/SKILL.md`](@warlock.js/cascade/paginate-results/SKILL.md).
|
|
1273
|
+
|
|
1274
|
+
## Count and existence
|
|
1275
|
+
|
|
1276
|
+
```ts
|
|
1277
|
+
const total = await User.count();
|
|
1278
|
+
const activeCount = await User.count({ status: "active" });
|
|
1279
|
+
const adminCount = await User.where("role", "admin").count();
|
|
1280
|
+
|
|
1281
|
+
const hasAdmin = await User.where("role", "admin").exists(); // boolean, short-circuits
|
|
1282
|
+
const noneBlocked = await User.where("status", "blocked").notExists();
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1285
|
+
**Don't reach for `.count() > 0`** when you only need a boolean — `.exists()` short-circuits on the first matching row. The difference shows up immediately on tables with more than a few thousand rows.
|
|
1286
|
+
|
|
1287
|
+
## Get many — `.all(filter?)`
|
|
1288
|
+
|
|
1289
|
+
```ts
|
|
1290
|
+
const allUsers = await User.all();
|
|
1291
|
+
const activeUsers = await User.all({ status: "active" });
|
|
1292
|
+
```
|
|
1293
|
+
|
|
1294
|
+
`Model.all(filter?)` is the shortcut for "fetch all records matching a simple equality filter, or every record if no filter."
|
|
1295
|
+
|
|
1296
|
+
**Caution.** `.all()` with no filter loads the entire table. Use [pagination](@warlock.js/cascade/paginate-results/SKILL.md) for tables larger than a few hundred rows.
|
|
1297
|
+
|
|
1298
|
+
## The wider query vocabulary
|
|
1299
|
+
|
|
1300
|
+
Cascade's query builder has around 60 methods. Reach for these as the need arises:
|
|
1301
|
+
|
|
1302
|
+
| Reach for | When |
|
|
1303
|
+
| --- | --- |
|
|
1304
|
+
| `.whereIn(field, values)` / `.whereNotIn(field, values)` | Match against / exclude a list |
|
|
1305
|
+
| `.whereNull(field)` / `.whereNotNull(field)` | Nullability checks |
|
|
1306
|
+
| `.whereBetween(field, [a, b])` | Inclusive range |
|
|
1307
|
+
| `.whereDate(field, value)`, `.whereDateBetween`, `.whereDateBefore`, `.whereDateAfter` | Date helpers |
|
|
1308
|
+
| `.whereLike(field, pattern)` / `.whereStartsWith` / `.whereEndsWith` | Pattern matching |
|
|
1309
|
+
| `.whereHas(relation, callback)` | Filter by conditions on a related model |
|
|
1310
|
+
| `.sum(field)` / `.avg(field)` / `.min(field)` / `.max(field)` | Aggregates — [`use-aggregates`](@warlock.js/cascade/aggregate-data/SKILL.md) |
|
|
1311
|
+
| `.distinct(field)` / `.pluck(field)` | Single-field reads (distinct values, flat list) |
|
|
1312
|
+
| `.chunk(size, callback)` | Stream a large table in batches |
|
|
1313
|
+
| `.cursorPaginate({ limit, cursor })` | Cursor pagination — [`paginate-results`](@warlock.js/cascade/paginate-results/SKILL.md) |
|
|
1314
|
+
| `.similarTo(column, embedding)` | Vector similarity — [`use-vector-search`](@warlock.js/cascade/search-by-vector/SKILL.md) |
|
|
1315
|
+
|
|
1316
|
+
Each chains off `User.where(...)` or `User.query()` and ends with the appropriate terminator. (`where`, `with`, `joinWith`, `first`, `count`, `find`, `all`, `paginate` are static shortcuts on the model; the rest live on the query builder, so reach them via `User.query()` or by chaining off a static `where`.)
|
|
1317
|
+
|
|
1318
|
+
## Scopes — reusable query fragments
|
|
1319
|
+
|
|
1320
|
+
When you write the same `.where("status", "active")` across multiple services, define a scope on the model:
|
|
1321
|
+
|
|
1322
|
+
```ts
|
|
1323
|
+
@RegisterModel()
|
|
1324
|
+
export class User extends Model<UserSchema> {
|
|
1325
|
+
public static table = "users";
|
|
1326
|
+
public static schema = userSchema;
|
|
1327
|
+
|
|
1328
|
+
static {
|
|
1329
|
+
this.addScope("active", (query) => {
|
|
1330
|
+
query.where("status", "active");
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const activeUsers = await User.query().scope("active").get();
|
|
1336
|
+
```
|
|
1337
|
+
|
|
1338
|
+
**Local scopes** (`addScope`) — opt-in, only when you call `.scope("name")`.
|
|
1339
|
+
**Global scopes** (`addGlobalScope`) — run on every query for that model. Useful for multi-tenancy or default soft-delete filtering. Bypass per-query with `.withoutGlobalScope("name")` / `.withoutGlobalScopes()`.
|
|
1340
|
+
|
|
1341
|
+
## Things NOT to do
|
|
1342
|
+
|
|
1343
|
+
- Don't `.count() > 0` for existence — use `.exists()`.
|
|
1344
|
+
- Don't `Model.all()` without a filter on a production table — use pagination or chunking.
|
|
1345
|
+
- Don't `!` away the null from `.find()` / `.first()` — handle the missing case explicitly or use `.firstOrFail()` when absence is a real error.
|
|
1346
|
+
- Don't write the same filter chain across multiple services — promote it to a scope.
|
|
1347
|
+
|
|
1348
|
+
## See also
|
|
1349
|
+
|
|
1350
|
+
- [`@warlock.js/cascade/define-relations/SKILL.md`](@warlock.js/cascade/define-relations/SKILL.md) — `.with(...)`, `.whereHas(...)`, eager loading
|
|
1351
|
+
- [`@warlock.js/cascade/paginate-results/SKILL.md`](@warlock.js/cascade/paginate-results/SKILL.md) — pagination + cursor + chunk
|
|
1352
|
+
- [`@warlock.js/cascade/aggregate-data/SKILL.md`](@warlock.js/cascade/aggregate-data/SKILL.md) — `.sum`, `.avg`, `.groupBy`, `.having`
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
## run-cascade-cli `@warlock.js/cascade/run-cascade-cli/SKILL.md`
|
|
1356
|
+
|
|
1357
|
+
---
|
|
1358
|
+
name: run-cascade-cli
|
|
1359
|
+
description: 'Cascade''s standalone `cascade` binary + the Operations API it wraps — `cascade migrate` / `migrate:list` / `migrate:rollback` / `migrate:export-sql`, and `runMigrations` / `rollbackMigrations` / `freshMigrate` / `exportMigrationsSQL` / `listExecutedMigrations` / `createDatabase` / `dropAllTables` / `migrationRunner`. Triggers: `cascade migrate`, `migrate:list`, `migrate:rollback`, `migrate:export-sql`, `runMigrations`, `rollbackMigrations`, `freshMigrate`, `exportMigrationsSQL`, `listExecutedMigrations`, `migrationRunner`; "run migrations in deploy/CI", "reset DB for tests", "programmatic migration", "foreign key constraint cannot be implemented", `CASCADE_PRIMARY_KEY`; typical import `import { runMigrations, migrationRunner } from "@warlock.js/cascade"`. Skip: writing migration files — `@warlock.js/cascade/write-migration/SKILL.md`; competing tools `knex migrate:latest`, `prisma migrate deploy`, `typeorm migration:run`.'
|
|
1360
|
+
---
|
|
1361
|
+
|
|
1362
|
+
# Run the cascade CLI / Operations API
|
|
1363
|
+
|
|
1364
|
+
Cascade ships a standalone `cascade` binary plus a programmatic **Operations API** — named functions over the migration-runner singleton. The binary is a thin wrapper over those functions; warlock-core's CLI wraps the same code path. Use the binary from terminal / deploy scripts; use the Operations API from test setup, container init, or custom tooling.
|
|
1365
|
+
|
|
1366
|
+
## CLI commands
|
|
1367
|
+
|
|
1368
|
+
The standalone binary exposes four colon-keyed subcommands:
|
|
1369
|
+
|
|
1370
|
+
```bash
|
|
1371
|
+
cascade migrate # run all pending migrations
|
|
1372
|
+
cascade migrate:list # show executed migrations from _migrations
|
|
1373
|
+
cascade migrate:rollback # undo the last batch
|
|
1374
|
+
cascade migrate:export-sql # write .up.sql / .down.sql instead of executing
|
|
1375
|
+
```
|
|
1376
|
+
|
|
1377
|
+
Flags:
|
|
1378
|
+
- `migrate` — `-f/--fresh` (drop everything and re-run), `-s/--sql` (export SQL instead of executing), `--pending-only`, `-c/--compact`, `-p/--path <glob>`
|
|
1379
|
+
- `migrate:rollback` — `-a/--all` (roll back everything), `--batches N`, `-p/--path <glob>`
|
|
1380
|
+
- `migrate:export-sql` — `--pending-only`, `-c/--compact`, `-p/--path <glob>`
|
|
1381
|
+
|
|
1382
|
+
There is **no** `seed`, `db:create`, or `db:drop-tables` in the standalone binary — those need project context; use the warlock CLI when you have a Warlock app.
|
|
1383
|
+
|
|
1384
|
+
## Configuration — env vars only
|
|
1385
|
+
|
|
1386
|
+
No `cascade.config.{ts,js}` file. The CLI auto-loads `.env` from cwd at start.
|
|
1387
|
+
|
|
1388
|
+
```bash
|
|
1389
|
+
DATABASE_URL=postgres://user:pass@host:5432/db # one connection string …
|
|
1390
|
+
# … or discrete vars:
|
|
1391
|
+
DB_DIALECT=postgres # or mongodb
|
|
1392
|
+
DB_HOST=localhost
|
|
1393
|
+
DB_PORT=5432
|
|
1394
|
+
DB_NAME=myapp
|
|
1395
|
+
DB_USER=postgres
|
|
1396
|
+
DB_PASSWORD=secret
|
|
1397
|
+
|
|
1398
|
+
# Migration defaults — only set when overriding library defaults:
|
|
1399
|
+
CASCADE_PRIMARY_KEY=uuid # uuid | int | bigInt
|
|
1400
|
+
CASCADE_UUID_STRATEGY=v7 # v4 | v7
|
|
1401
|
+
```
|
|
1402
|
+
|
|
1403
|
+
Warlock-project aliases accepted: `DB_URL` ↔ `DATABASE_URL`, `DB_DRIVER` ↔ `DB_DIALECT`, `DB_USERNAME` ↔ `DB_USER`.
|
|
1404
|
+
|
|
1405
|
+
## Diagnose first: "foreign key constraint … cannot be implemented"
|
|
1406
|
+
|
|
1407
|
+
On a fresh Postgres run this is almost always a primary-key type mismatch: a migration declared a `uuid()` foreign key, but the referenced table's PK got created as `bigserial` because `migrationDefaults.primaryKey` defaulted to `"int"`. Fix by matching the project's PK convention via env:
|
|
1408
|
+
|
|
1409
|
+
```bash
|
|
1410
|
+
CASCADE_PRIMARY_KEY=uuid
|
|
1411
|
+
CASCADE_UUID_STRATEGY=v7
|
|
1412
|
+
```
|
|
1413
|
+
|
|
1414
|
+
Warlock projects set these in `src/config/database.ts`'s `migrationOptions`; the cascade CLI mirrors them via env — match the values exactly.
|
|
1415
|
+
|
|
1416
|
+
## TS migrations — invoke through a TS runtime
|
|
1417
|
+
|
|
1418
|
+
The cascade CLI ships **no TypeScript transpiler.** For `.ts` migrations, invoke through `tsx` / `ts-node` / any TS-aware runtime:
|
|
1419
|
+
|
|
1420
|
+
```bash
|
|
1421
|
+
npx tsx node_modules/.bin/cascade migrate
|
|
1422
|
+
```
|
|
1423
|
+
|
|
1424
|
+
If forgotten, cascade catches the import failure and prints a pointer to this pattern.
|
|
1425
|
+
|
|
1426
|
+
## Migration file discovery
|
|
1427
|
+
|
|
1428
|
+
Default glob: `./migrations/**/*.{ts,js,mjs,cjs}` from cwd. Override with `-p`, and **always quote the pattern** (the shell expands `**`/`*` before the binary sees it otherwise):
|
|
1429
|
+
|
|
1430
|
+
```bash
|
|
1431
|
+
cascade migrate -p "src/app/**/migrations/*.ts"
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
Each file must `export default` a migration class; cascade infers the name from the filename and uses any leading `MM-DD-YYYY_HH-MM-SS` timestamp for ordering.
|
|
1435
|
+
|
|
1436
|
+
## Operations API — programmatic equivalents
|
|
1437
|
+
|
|
1438
|
+
```ts
|
|
1439
|
+
import {
|
|
1440
|
+
runMigrations,
|
|
1441
|
+
rollbackMigrations,
|
|
1442
|
+
freshMigrate,
|
|
1443
|
+
exportMigrationsSQL,
|
|
1444
|
+
listExecutedMigrations,
|
|
1445
|
+
createDatabase,
|
|
1446
|
+
dropAllTables,
|
|
1447
|
+
migrationRunner,
|
|
1448
|
+
} from "@warlock.js/cascade";
|
|
1449
|
+
import CreateUsersTable from "./migrations/create-users.migration";
|
|
1450
|
+
|
|
1451
|
+
migrationRunner.registerMany([CreateUsersTable /* … */]);
|
|
1452
|
+
|
|
1453
|
+
const results = await runMigrations();
|
|
1454
|
+
const failed = results.filter((r) => !r.success);
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
Reach for these when you need migrations inside test setup (`beforeAll(async () => { await runMigrations(); })`), container init scripts, custom CLI wrappers, or reading `_migrations` programmatically (`listExecutedMigrations()`). The Operations API returns structured data and **does not print** — the caller decides how to surface progress. (The runner still emits per-migration logs through `@warlock.js/logger`.)
|
|
1458
|
+
|
|
1459
|
+
## Common task → command
|
|
1460
|
+
|
|
1461
|
+
| You want to… | Command |
|
|
1462
|
+
|---|---|
|
|
1463
|
+
| Run all pending migrations | `cascade migrate` |
|
|
1464
|
+
| Drop everything and re-run | `cascade migrate -f` |
|
|
1465
|
+
| Roll back the last batch | `cascade migrate:rollback` |
|
|
1466
|
+
| Roll back everything | `cascade migrate:rollback --all` |
|
|
1467
|
+
| Generate SQL without executing | `cascade migrate --sql` |
|
|
1468
|
+
| Generate SQL for pending only | `cascade migrate --sql --pending-only` |
|
|
1469
|
+
| See what's been executed | `cascade migrate:list` |
|
|
1470
|
+
| Run from a non-default folder | `cascade migrate -p "<glob>"` |
|
|
1471
|
+
|
|
1472
|
+
## Things NOT to do
|
|
1473
|
+
|
|
1474
|
+
- Don't run `cascade migrate -f` (fresh) anywhere that could touch production — it drops everything first.
|
|
1475
|
+
- Don't run `migrate` from inside app code at boot. Migrations are a deploy step; coupling them to boot makes rolling restarts dangerous.
|
|
1476
|
+
- Don't forget to quote `-p` globs, or the shell expands them and cascade registers a single file.
|
|
1477
|
+
|
|
1478
|
+
## See also
|
|
1479
|
+
|
|
1480
|
+
- [`@warlock.js/cascade/write-migration/SKILL.md`](@warlock.js/cascade/write-migration/SKILL.md) — writing migration files
|
|
1481
|
+
- [`@warlock.js/cascade/manage-data-sources/SKILL.md`](@warlock.js/cascade/manage-data-sources/SKILL.md) — multi-database routing
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
## search-by-vector `@warlock.js/cascade/search-by-vector/SKILL.md`
|
|
1485
|
+
|
|
1486
|
+
---
|
|
1487
|
+
name: search-by-vector
|
|
1488
|
+
description: 'Vector similarity search via `.similarTo(column, embedding, alias?)` — adds a similarity `score` column and orders by vector distance so the index is used; cap results with `.limit()`. Postgres uses pgvector (IVFFlat index via `this.vectorIndex`); MongoDB needs Atlas. Schema: `this.vector(column, dimensions)` + `this.vectorIndex(column, { dimensions, similarity })`. Triggers: `.similarTo`, `this.vector`, `this.vectorIndex`, `.whereFullText`, pgvector; "semantic search", "RAG retrieval", "find similar articles", "hybrid vector + full-text"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: query basics — `@warlock.js/cascade/query-data/SKILL.md`; semantic cache — `@warlock.js/cache/use-cache-similarity/SKILL.md`; competing libs `pgvector` directly, `chromadb`, `pinecone`, `weaviate`, `qdrant`.'
|
|
1489
|
+
---
|
|
1490
|
+
|
|
1491
|
+
# Use vector search
|
|
1492
|
+
|
|
1493
|
+
Query by vector distance for semantic search. Cascade gives you the column type, the index, and the similarity query method — generating embeddings is your AI provider's job.
|
|
1494
|
+
|
|
1495
|
+
## Schema + migration
|
|
1496
|
+
|
|
1497
|
+
Both the column and its index are builders on the migration `this`:
|
|
1498
|
+
|
|
1499
|
+
```ts
|
|
1500
|
+
import { Migration } from "@warlock.js/cascade";
|
|
1501
|
+
|
|
1502
|
+
export default class CreateArticles extends Migration {
|
|
1503
|
+
public readonly table = "articles";
|
|
1504
|
+
|
|
1505
|
+
public up(): void {
|
|
1506
|
+
this.createTable();
|
|
1507
|
+
this.id();
|
|
1508
|
+
this.string("title");
|
|
1509
|
+
this.text("body");
|
|
1510
|
+
this.vector("embedding", 1536); // pgvector column, 1536 dims
|
|
1511
|
+
this.vectorIndex("embedding", { dimensions: 1536, similarity: "cosine" });
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
public down(): void {
|
|
1515
|
+
this.dropTable();
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
```
|
|
1519
|
+
|
|
1520
|
+
`vectorIndex(column, { dimensions, similarity?, lists?, name? })` — `similarity` is `"cosine" | "euclidean" | "dotProduct"` (maps to the pgvector operator class). On Postgres this builds an **IVFFlat** index (`lists` controls the cluster count, default 100). Requires `CREATE EXTENSION vector` on the database.
|
|
1521
|
+
|
|
1522
|
+
On MongoDB the vector index is an Atlas Search index definition (Atlas-only).
|
|
1523
|
+
|
|
1524
|
+
## Write — store an embedding
|
|
1525
|
+
|
|
1526
|
+
```ts
|
|
1527
|
+
const embedding = await ai.embed(body);
|
|
1528
|
+
await Article.create({ title, body, embedding });
|
|
1529
|
+
```
|
|
1530
|
+
|
|
1531
|
+
Cascade stores the vector and queries against it; it doesn't compute embeddings.
|
|
1532
|
+
|
|
1533
|
+
## Read — similarity search
|
|
1534
|
+
|
|
1535
|
+
`.similarTo(column, embedding, alias?)` does two things at once: it adds `1 - (column <=> embedding) AS score` to the SELECT so each row carries its similarity, and it adds `ORDER BY column <=> embedding` so the database uses the vector index instead of a sequential scan. Cap the result with `.limit()`:
|
|
1536
|
+
|
|
1537
|
+
```ts
|
|
1538
|
+
const queryEmbedding = await ai.embed("how does cascade vector search work?");
|
|
1539
|
+
|
|
1540
|
+
const hits = await Article.query()
|
|
1541
|
+
.similarTo("embedding", queryEmbedding) // score column defaults to "score"
|
|
1542
|
+
.limit(5)
|
|
1543
|
+
.get<ArticleRow & { score: number }>();
|
|
1544
|
+
|
|
1545
|
+
// hits[0].score → similarity of the closest match
|
|
1546
|
+
```
|
|
1547
|
+
|
|
1548
|
+
There is no options object — `topK` is just `.limit(k)`, and the distance metric is fixed at index creation (the `similarity` you passed to `vectorIndex`). The third argument only renames the score column (`.similarTo("embedding", vec, "distance")`). Don't add your own `.orderBy()` on the score alias afterward — it would break index usage.
|
|
1549
|
+
|
|
1550
|
+
## Filtered similarity
|
|
1551
|
+
|
|
1552
|
+
Chain `.where()` before `.similarTo()`:
|
|
1553
|
+
|
|
1554
|
+
```ts
|
|
1555
|
+
const tenantHits = await Article.query()
|
|
1556
|
+
.where("tenant_id", tenantId)
|
|
1557
|
+
.where("published", true)
|
|
1558
|
+
.similarTo("embedding", queryEmbedding)
|
|
1559
|
+
.limit(5)
|
|
1560
|
+
.get<ArticleRow & { score: number }>();
|
|
1561
|
+
```
|
|
1562
|
+
|
|
1563
|
+
The DB applies the filter first (regular index), then ranks the remaining candidates by similarity (vector index).
|
|
1564
|
+
|
|
1565
|
+
## Hybrid search — vector + full-text
|
|
1566
|
+
|
|
1567
|
+
Cascade has `.whereFullText(fields, query)` for the text side. For best retrieval quality on long-form text, run a vector search and a full-text search and combine the results in code (re-rank or reciprocal-rank-fusion).
|
|
1568
|
+
|
|
1569
|
+
## RAG — retrieval-augmented generation
|
|
1570
|
+
|
|
1571
|
+
```ts
|
|
1572
|
+
async function answer(question: string) {
|
|
1573
|
+
const queryEmbedding = await ai.embed(question);
|
|
1574
|
+
|
|
1575
|
+
const context = await Document.query()
|
|
1576
|
+
.where("tenant_id", currentTenant.id)
|
|
1577
|
+
.similarTo("embedding", queryEmbedding)
|
|
1578
|
+
.limit(8)
|
|
1579
|
+
.get<DocumentRow & { score: number }>();
|
|
1580
|
+
|
|
1581
|
+
// optional: drop low-similarity hits in code
|
|
1582
|
+
const relevant = context.filter((document) => document.score >= 0.75);
|
|
1583
|
+
|
|
1584
|
+
const prompt = buildPrompt(question, relevant.map((document) => document.body));
|
|
1585
|
+
return ai.complete(prompt);
|
|
1586
|
+
}
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1589
|
+
A score threshold isn't a query option — filter on the `score` column in code after the fetch.
|
|
1590
|
+
|
|
1591
|
+
## Driver support
|
|
1592
|
+
|
|
1593
|
+
| Driver | Vector |
|
|
1594
|
+
| --- | --- |
|
|
1595
|
+
| Postgres (with `CREATE EXTENSION vector`) | ✅ pgvector + IVFFlat index |
|
|
1596
|
+
| MongoDB Atlas (paid tier + Atlas Search index) | ✅ `$vectorSearch` aggregation stage |
|
|
1597
|
+
| MongoDB community / self-hosted / local | ❌ Atlas-only |
|
|
1598
|
+
|
|
1599
|
+
For local dev with MongoDB, develop the vector path against Postgres + pgvector.
|
|
1600
|
+
|
|
1601
|
+
## Things NOT to do
|
|
1602
|
+
|
|
1603
|
+
- Don't pass `{ topK, metric, threshold }` to `.similarTo()` — it takes `(column, embedding, alias?)`. Use `.limit()` for topK, set the metric at `vectorIndex` creation, and threshold in code on the `score` column.
|
|
1604
|
+
- Don't `.orderBy()` the score alias after `.similarTo()` — it already orders by distance for index usage.
|
|
1605
|
+
- Don't re-embed an entire corpus when changing embedding models — vectors aren't portable across models; plan the migration.
|
|
1606
|
+
- Don't ship the raw vector array to clients. Drop it from the public shape with `static toJsonColumns`.
|
|
1607
|
+
- Don't expect `.similarTo()` without a vector index to scale. Above a few thousand rows, sequential scans dominate.
|
|
1608
|
+
|
|
1609
|
+
## See also
|
|
1610
|
+
|
|
1611
|
+
- [`@warlock.js/cascade/query-data/SKILL.md`](@warlock.js/cascade/query-data/SKILL.md) — `.where`, `.whereFullText`, the broader query vocabulary
|
|
1612
|
+
- [`@warlock.js/cache/use-cache-similarity/SKILL.md`](@warlock.js/cache/use-cache-similarity/SKILL.md) — semantic cache of LLM output
|
|
1613
|
+
|
|
1614
|
+
|
|
1615
|
+
## subscribe-to-model-events `@warlock.js/cascade/subscribe-to-model-events/SKILL.md`
|
|
1616
|
+
|
|
1617
|
+
---
|
|
1618
|
+
name: subscribe-to-model-events
|
|
1619
|
+
description: 'Hook into model lifecycle events — `saving` / `saved`, `creating` / `created`, `updating` / `updated`, `validating` / `validated`, `deleting` / `deleted`, `restoring` / `restored`, `fetching` / `fetched`. Per-model `Model.on(event, fn)` or global via `Model.globalEvents()`. Triggers: `Model.on`, `Model.off`, `saving`, `saved`, `created`, `updated`, `deleting`, `deleted`, `restored`; "audit log on save", "notify on change", "denormalize into search index"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: dirty tracking — `@warlock.js/cascade/track-changes/SKILL.md`; competing libs `mongoose` middleware, `typeorm` subscribers, `prisma` extensions.'
|
|
1620
|
+
---
|
|
1621
|
+
|
|
1622
|
+
# Use model events
|
|
1623
|
+
|
|
1624
|
+
Every meaningful moment in a model's lifecycle — about to validate, about to save, just saved, just deleted — fires an event. Subscribe to hook in cross-cutting behavior without scattering it across every service.
|
|
1625
|
+
|
|
1626
|
+
## Subscribing — per-model
|
|
1627
|
+
|
|
1628
|
+
```ts
|
|
1629
|
+
User.on("saved", async (user) => {
|
|
1630
|
+
await searchIndex.upsert({ id: user.id, name: user.get("name") });
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
User.on("deleted", async (user) => {
|
|
1634
|
+
await searchIndex.remove(user.id);
|
|
1635
|
+
});
|
|
1636
|
+
```
|
|
1637
|
+
|
|
1638
|
+
Listeners are async-aware — the model's persistence awaits the listener before moving on. A listener that throws during a pre-write event (`saving` / `creating` / `updating` / `validating`) aborts the operation; the error propagates to the caller.
|
|
1639
|
+
|
|
1640
|
+
## The full event catalog
|
|
1641
|
+
|
|
1642
|
+
The events fire in this order. The **gerund** form (`saving`, `creating`) is the "before" hook — there is no `beforeSave` alias.
|
|
1643
|
+
|
|
1644
|
+
| Event | Fires |
|
|
1645
|
+
| --- | --- |
|
|
1646
|
+
| `saving` | Before validation, on every `save()` (both insert and update paths) |
|
|
1647
|
+
| `validating` / `validated` | Around schema validation |
|
|
1648
|
+
| `creating` | Before a new record is inserted (first save) |
|
|
1649
|
+
| `updating` | Before an existing record's update is written |
|
|
1650
|
+
| `saved` | After a successful write (any branch — insert or update) |
|
|
1651
|
+
| `created` | After a new record is inserted |
|
|
1652
|
+
| `updated` | After an existing record's update is written |
|
|
1653
|
+
| `deleting` / `deleted` | Before / after the delete strategy runs |
|
|
1654
|
+
| `restoring` / `restored` | Before / after a soft-deleted or trashed record is restored |
|
|
1655
|
+
| `fetching` / `fetched` / `hydrating` | Around reads / instance hydration |
|
|
1656
|
+
|
|
1657
|
+
`saving` fires for both inserts and updates; `creating` / `updating` narrow to the branch. So `saved` always fires; exactly one of `created` / `updated` follows.
|
|
1658
|
+
|
|
1659
|
+
## Listener signature
|
|
1660
|
+
|
|
1661
|
+
```ts
|
|
1662
|
+
User.on("saving", async (user, context) => {
|
|
1663
|
+
// user: the User instance about to be persisted
|
|
1664
|
+
// context: { isInsert, options, mode } for `saving`; varies per event
|
|
1665
|
+
});
|
|
1666
|
+
```
|
|
1667
|
+
|
|
1668
|
+
The first argument is the model instance. The second carries event context (for `saving`: whether it's an insert, the `save()` options, and the mode).
|
|
1669
|
+
|
|
1670
|
+
## Throwing to abort
|
|
1671
|
+
|
|
1672
|
+
A listener on a pre-write event that throws stops the lifecycle — the save / delete never completes and the error propagates:
|
|
1673
|
+
|
|
1674
|
+
```ts
|
|
1675
|
+
User.on("saving", async (user) => {
|
|
1676
|
+
if (user.isDirty("email") && (await emailIsBlacklisted(user.get("email")))) {
|
|
1677
|
+
throw new Error("Email domain is blacklisted");
|
|
1678
|
+
}
|
|
1679
|
+
});
|
|
1680
|
+
```
|
|
1681
|
+
|
|
1682
|
+
Pair with `isDirty()` from [`@warlock.js/cascade/track-changes/SKILL.md`](@warlock.js/cascade/track-changes/SKILL.md) so the listener only runs its expensive check when the relevant field changed.
|
|
1683
|
+
|
|
1684
|
+
## Global listeners — across all models
|
|
1685
|
+
|
|
1686
|
+
For framework-level concerns (audit log, observability, cache invalidation), subscribe on the base `Model` — the listener runs for every model:
|
|
1687
|
+
|
|
1688
|
+
```ts
|
|
1689
|
+
import { Model } from "@warlock.js/cascade";
|
|
1690
|
+
|
|
1691
|
+
Model.on("saved", async (instance) => {
|
|
1692
|
+
await audit.log("save", instance.constructor.name, instance.id, instance.getDirtyColumns());
|
|
1693
|
+
});
|
|
1694
|
+
```
|
|
1695
|
+
|
|
1696
|
+
Filter by `instance.constructor.name === "User"` when you only care about specific models, or register per-model handlers when the scope is narrow. (`Model.globalEvents()` returns the underlying global emitter if you need direct access.)
|
|
1697
|
+
|
|
1698
|
+
## `off()` to unsubscribe
|
|
1699
|
+
|
|
1700
|
+
```ts
|
|
1701
|
+
const handler = async (user) => {
|
|
1702
|
+
/* ... */
|
|
1703
|
+
};
|
|
1704
|
+
const unsubscribe = User.on("saved", handler);
|
|
1705
|
+
|
|
1706
|
+
// later — either:
|
|
1707
|
+
unsubscribe();
|
|
1708
|
+
// or:
|
|
1709
|
+
User.off("saved", handler);
|
|
1710
|
+
```
|
|
1711
|
+
|
|
1712
|
+
`on()` also returns an unsubscribe function. Mostly useful in tests where you want to temporarily swap behavior.
|
|
1713
|
+
|
|
1714
|
+
## Common patterns
|
|
1715
|
+
|
|
1716
|
+
### Audit log (combined with dirty tracking)
|
|
1717
|
+
|
|
1718
|
+
```ts
|
|
1719
|
+
Model.on("updated", async (instance) => {
|
|
1720
|
+
const changes = instance.getDirtyColumnsWithValues();
|
|
1721
|
+
if (Object.keys(changes).length === 0) {
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
await AuditLog.create({
|
|
1726
|
+
model: instance.constructor.name,
|
|
1727
|
+
record_id: instance.id,
|
|
1728
|
+
changes,
|
|
1729
|
+
saved_at: new Date(),
|
|
1730
|
+
});
|
|
1731
|
+
});
|
|
1732
|
+
```
|
|
1733
|
+
|
|
1734
|
+
Use `updated` (not `saved`) when you only want change diffs — `getDirtyColumnsWithValues()` is still populated in the post-write events because the tracker resets *after* they fire.
|
|
1735
|
+
|
|
1736
|
+
### Cache invalidation
|
|
1737
|
+
|
|
1738
|
+
```ts
|
|
1739
|
+
User.on("saved", async (user) => {
|
|
1740
|
+
await cache.tags([`user.${user.id}`]).invalidate();
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1743
|
+
User.on("deleted", async (user) => {
|
|
1744
|
+
await cache.tags([`user.${user.id}`]).invalidate();
|
|
1745
|
+
});
|
|
1746
|
+
```
|
|
1747
|
+
|
|
1748
|
+
See [`@warlock.js/cache/use-cache-tags/SKILL.md`](@warlock.js/cache/use-cache-tags/SKILL.md).
|
|
1749
|
+
|
|
1750
|
+
### Side effects that must only fire after commit
|
|
1751
|
+
|
|
1752
|
+
For external side effects (queues, emails) that must only fire if a surrounding transaction commits, don't run them in the `saved` handler — write to an outbox table and dispatch from a worker after commit.
|
|
1753
|
+
|
|
1754
|
+
## Things NOT to do
|
|
1755
|
+
|
|
1756
|
+
- Don't reach for `beforeSave` / `destroying` / `destroyed` — they don't exist. The hooks are `saving` (pre-save) and `deleting` / `deleted`.
|
|
1757
|
+
- Don't run long external work (HTTP calls, queue dispatches) directly in `saving` / `saved` handlers — inside a transaction they extend the lock. Use an outbox table.
|
|
1758
|
+
- Don't mutate the model's own fields in `saved` and expect them to persist. The write already happened; mutations here are in-memory only. Use `saving` for pre-persist mutation.
|
|
1759
|
+
- Don't subscribe inside a function that runs on every request. Register handlers once at startup, in a dedicated init file.
|
|
1760
|
+
|
|
1761
|
+
## See also
|
|
1762
|
+
|
|
1763
|
+
- [`@warlock.js/cascade/track-changes/SKILL.md`](@warlock.js/cascade/track-changes/SKILL.md) — `isDirty` / `getDirtyColumnsWithValues` for "only run if this field changed"
|
|
1764
|
+
- [`@warlock.js/cascade/configure-delete-strategy/SKILL.md`](@warlock.js/cascade/configure-delete-strategy/SKILL.md) — `deleting` / `deleted` / `restored` around delete strategies
|
|
1765
|
+
|
|
1766
|
+
|
|
1767
|
+
## track-changes `@warlock.js/cascade/track-changes/SKILL.md`
|
|
1768
|
+
|
|
1769
|
+
---
|
|
1770
|
+
name: track-changes
|
|
1771
|
+
description: 'Inspect a model''s pending changes — `hasChanges()` (any field dirty?), `isDirty(column)` (one column), `getDirtyColumns()` (changed field names), `getDirtyColumnsWithValues()` (old + new per field), `getRemovedColumns()` (unset fields). Triggers: `hasChanges`, `isDirty`, `getDirtyColumns`, `getDirtyColumnsWithValues`, `getRemovedColumns`; "only run if email changed", "diff for an audit log", "what fields are dirty", "compare old vs new value"; typical import `import { Model } from "@warlock.js/cascade"`. Skip: hooking into save — `@warlock.js/cascade/subscribe-to-model-events/SKILL.md`; update idioms — `@warlock.js/cascade/define-model/SKILL.md`; competing libs `mongoose` `isModified` / `modifiedPaths`, `typeorm` change detection.'
|
|
1772
|
+
---
|
|
1773
|
+
|
|
1774
|
+
# Track changes
|
|
1775
|
+
|
|
1776
|
+
Every Cascade model carries a dirty tracker that records which fields you've changed since the model was loaded (or since the last `save()`). The tracker is the source of truth for "what would change if I save now."
|
|
1777
|
+
|
|
1778
|
+
## The main reads
|
|
1779
|
+
|
|
1780
|
+
```ts
|
|
1781
|
+
user.hasChanges(); // boolean — any field changed (or removed)?
|
|
1782
|
+
user.isDirty("email"); // boolean — specifically this column?
|
|
1783
|
+
|
|
1784
|
+
user.getDirtyColumns(); // string[] — names of changed columns
|
|
1785
|
+
user.getRemovedColumns(); // string[] — columns explicitly unset since load
|
|
1786
|
+
|
|
1787
|
+
user.getDirtyColumnsWithValues();
|
|
1788
|
+
// Record<string, { oldValue, newValue }> — the full diff, old + new per column
|
|
1789
|
+
```
|
|
1790
|
+
|
|
1791
|
+
`isDirty` takes **one** column. To check several, ask `getDirtyColumns()` once and test membership (see the multi-field section below).
|
|
1792
|
+
|
|
1793
|
+
## Conditional logic before save
|
|
1794
|
+
|
|
1795
|
+
```ts
|
|
1796
|
+
if (user.isDirty("email")) {
|
|
1797
|
+
const { oldValue } = user.getDirtyColumnsWithValues().email;
|
|
1798
|
+
await mailer.sendEmailChangeNotice(oldValue);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
await user.save();
|
|
1802
|
+
```
|
|
1803
|
+
|
|
1804
|
+
The classic shape — only fire the side effect if the field actually changed. The pre-mutation value comes from `getDirtyColumnsWithValues()[column].oldValue`.
|
|
1805
|
+
|
|
1806
|
+
## Diff for an audit log
|
|
1807
|
+
|
|
1808
|
+
```ts
|
|
1809
|
+
const changes = user.getDirtyColumnsWithValues();
|
|
1810
|
+
// e.g. { name: { oldValue: "Ada", newValue: "Augusta Ada King" } }
|
|
1811
|
+
|
|
1812
|
+
await AuditLog.create({
|
|
1813
|
+
user_id: user.id,
|
|
1814
|
+
before: Object.fromEntries(
|
|
1815
|
+
Object.entries(changes).map(([field, { oldValue }]) => [field, oldValue]),
|
|
1816
|
+
),
|
|
1817
|
+
after: Object.fromEntries(
|
|
1818
|
+
Object.entries(changes).map(([field, { newValue }]) => [field, newValue]),
|
|
1819
|
+
),
|
|
1820
|
+
changed_at: new Date(),
|
|
1821
|
+
});
|
|
1822
|
+
```
|
|
1823
|
+
|
|
1824
|
+
`getDirtyColumnsWithValues()` hands you old and new together, so you never need a second call to read the prior value.
|
|
1825
|
+
|
|
1826
|
+
## After save — tracker resets
|
|
1827
|
+
|
|
1828
|
+
```ts
|
|
1829
|
+
user.set("name", "Augusta");
|
|
1830
|
+
user.isDirty("name"); // true
|
|
1831
|
+
await user.save();
|
|
1832
|
+
user.isDirty("name"); // false — tracker reset to the just-saved state
|
|
1833
|
+
```
|
|
1834
|
+
|
|
1835
|
+
`save()` resets the tracker's baseline to the persisted state. Subsequent `isDirty()` checks measure changes since the last `save()`, not since the original load.
|
|
1836
|
+
|
|
1837
|
+
## Multi-field check
|
|
1838
|
+
|
|
1839
|
+
`isDirty` is single-column. For "did any of these change?", read the dirty set once:
|
|
1840
|
+
|
|
1841
|
+
```ts
|
|
1842
|
+
const dirty = new Set(user.getDirtyColumns());
|
|
1843
|
+
const billingTouched = ["billing_address", "billing_city", "billing_zip"].some((field) =>
|
|
1844
|
+
dirty.has(field),
|
|
1845
|
+
);
|
|
1846
|
+
|
|
1847
|
+
if (billingTouched) {
|
|
1848
|
+
await revalidateBillingAddress(user);
|
|
1849
|
+
}
|
|
1850
|
+
```
|
|
1851
|
+
|
|
1852
|
+
## Tracker vs `.get()`
|
|
1853
|
+
|
|
1854
|
+
- `user.get("email")` — the **current** value (post-mutation if you called `.set` / `.merge`)
|
|
1855
|
+
- `user.getDirtyColumnsWithValues().email?.oldValue` — the **pre-mutation** value (whatever the DB had)
|
|
1856
|
+
- `user.getDirtyColumns()` — the **changed field names** (string array)
|
|
1857
|
+
- `user.getDirtyColumnsWithValues()` — the **full diff** (old + new per changed field)
|
|
1858
|
+
|
|
1859
|
+
If you need both old and new together:
|
|
1860
|
+
|
|
1861
|
+
```ts
|
|
1862
|
+
const dirty = user.getDirtyColumnsWithValues();
|
|
1863
|
+
for (const [field, { oldValue, newValue }] of Object.entries(dirty)) {
|
|
1864
|
+
// ... log, validate, conditionally re-route
|
|
1865
|
+
}
|
|
1866
|
+
```
|
|
1867
|
+
|
|
1868
|
+
## Things NOT to do
|
|
1869
|
+
|
|
1870
|
+
- Don't use `hasChanges()` / `isDirty()` after `save()` to verify the save persisted. The tracker resets to clean on save — these become false regardless. Read back from the DB if you need verification.
|
|
1871
|
+
- Don't compare `user.get(field) === oldValue` for change detection — call `isDirty(field)` instead. The tracker uses identity-aware comparison appropriate to the column type (deep-equal for objects/arrays).
|
|
1872
|
+
- Don't expect dirty tracking on relations. The tracker covers columns only. For relation changes, use lifecycle events or compare counts.
|
|
1873
|
+
|
|
1874
|
+
## See also
|
|
1875
|
+
|
|
1876
|
+
- [`@warlock.js/cascade/subscribe-to-model-events/SKILL.md`](@warlock.js/cascade/subscribe-to-model-events/SKILL.md) — combining with `saved` / `saving` for cross-cutting behavior
|
|
1877
|
+
- [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md) — the `.set` / `.merge` / `.save` idioms that stage changes
|
|
1878
|
+
|
|
1879
|
+
|
|
1880
|
+
## write-migration `@warlock.js/cascade/write-migration/SKILL.md`
|
|
1881
|
+
|
|
1882
|
+
---
|
|
1883
|
+
name: write-migration
|
|
1884
|
+
description: 'Write a Cascade migration — the declarative `Migration.create(Model, { columns })` / `Migration.alter(Model, { ... })` factory is the primary form (column helpers `string` / `text` / `uuid` / `integer` imported from cascade, chained with `.notNullable()` / `.unique()` / `.references()`); the `extends Migration` class form is the imperative escape hatch. Run with the `cascade migrate` CLI; pin a source via `public dataSource`. Triggers: `Migration.create`, `Migration.alter`, `string()`, `text()`, `uuid()`, `.references`, `extends Migration`, `cascade migrate`; "write a migration", "create the users table", "add a column", "rollback the last batch"; typical import `import { Migration, text, uuid } from "@warlock.js/cascade"`. Skip: running migrations programmatically — `@warlock.js/cascade/run-cascade-cli/SKILL.md`; per-source migrations — `@warlock.js/cascade/manage-data-sources/SKILL.md`; competing tools `knex migrate`, `prisma migrate`, `typeorm migration`.'
|
|
1885
|
+
---
|
|
1886
|
+
|
|
1887
|
+
# Write a migration
|
|
1888
|
+
|
|
1889
|
+
The model class doesn't auto-create tables. Migrations declare the schema change in a versioned file; the CLI applies them in order for a reproducible DB shape across environments.
|
|
1890
|
+
|
|
1891
|
+
The primary form is **declarative**: `Migration.create(Model, { columns })` reads the table name from the model and builds the columns from the object you pass. Reach for the imperative `extends Migration` class form only when a migration is genuinely procedural.
|
|
1892
|
+
|
|
1893
|
+
## Minimal example — `Migration.create`
|
|
1894
|
+
|
|
1895
|
+
```ts title="src/app/users/models/user/migrations/05-11-2026_10-00-00-user.migration.ts"
|
|
1896
|
+
import { Migration, text, uuid } from "@warlock.js/cascade";
|
|
1897
|
+
import { User } from "../user.model";
|
|
1898
|
+
|
|
1899
|
+
export default Migration.create(User, {
|
|
1900
|
+
name: text().notNullable(),
|
|
1901
|
+
email: text().unique().notNullable(),
|
|
1902
|
+
status: text().notNullable(),
|
|
1903
|
+
});
|
|
1904
|
+
```
|
|
1905
|
+
|
|
1906
|
+
What happens:
|
|
1907
|
+
|
|
1908
|
+
- `Migration.create(Model, columns)` reads `User.table` for the table name and builds the DDL from the column map; it infers the rollback for you.
|
|
1909
|
+
- Column helpers (`text`, `uuid`, `string`, `integer`, …) are imported from `@warlock.js/cascade`. Each returns a builder you chain modifiers onto (`.notNullable()`, `.unique()`, `.nullable()`, `.default(...)`, `.references(table)`).
|
|
1910
|
+
- The `id` primary key and `createdAt` / `updatedAt` timestamps are added **automatically** — don't declare them. Naming follows the data source convention (snake_case on Postgres, camelCase on MongoDB).
|
|
1911
|
+
- `export default` is required — the runner imports each file's default export.
|
|
1912
|
+
|
|
1913
|
+
Evolve an existing table with `Migration.alter(Model, { ... })` (add / drop / rename / modify columns and indexes); it's declarative the same way.
|
|
1914
|
+
|
|
1915
|
+
## Running migrations
|
|
1916
|
+
|
|
1917
|
+
```bash
|
|
1918
|
+
yarn cascade migrate # apply pending migrations
|
|
1919
|
+
yarn cascade migrate:rollback # undo the last batch
|
|
1920
|
+
yarn cascade migrate:list # which migrations have been executed
|
|
1921
|
+
yarn cascade migrate:export-sql # write .up.sql / .down.sql instead of executing
|
|
1922
|
+
```
|
|
1923
|
+
|
|
1924
|
+
`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.
|
|
1925
|
+
|
|
1926
|
+
## File naming convention
|
|
1927
|
+
|
|
1928
|
+
Name files with a timestamp prefix (`MM-DD-YYYY_HH-MM-SS-<name>.migration.ts`) so Cascade orders runs deterministically and infers the migration name from the filename. A timestamp prefix prevents the "two devs picked the same number" merge conflict.
|
|
1929
|
+
|
|
1930
|
+
## Column helpers — the building blocks
|
|
1931
|
+
|
|
1932
|
+
Every column in a `Migration.create` / `Migration.alter` map starts with a helper imported from `@warlock.js/cascade`; each returns a builder you chain modifiers onto (`.notNullable()`, `.nullable()`, `.unique()`, `.default(value)`, `.index()`, `.primary()`, `.references(table)`):
|
|
1933
|
+
|
|
1934
|
+
```ts
|
|
1935
|
+
import { Migration, text, integer, json, timestamp, uuid } from "@warlock.js/cascade";
|
|
1936
|
+
import { User } from "../user.model";
|
|
1937
|
+
|
|
1938
|
+
export default Migration.create(Post, {
|
|
1939
|
+
title: text().notNullable(),
|
|
1940
|
+
body: text().nullable(),
|
|
1941
|
+
author_id: uuid().references(User.table).onDelete("cascade").notNullable(),
|
|
1942
|
+
status: text().notNullable(),
|
|
1943
|
+
metadata: json(), // JSON / JSONB column
|
|
1944
|
+
published_at: timestamp().nullable(),
|
|
1945
|
+
});
|
|
1946
|
+
```
|
|
1947
|
+
|
|
1948
|
+
`references(table)` defaults to the referenced table's `id` column; chain `.on("custom_id")` for a different one, and `.onDelete(...)` / `.onUpdate(...)` for FK actions. `id`, `createdAt`, and `updatedAt` are still added for you. The full helper vocabulary (`string`, `char`, `integer`, `bigInteger`, `decimal`, `boolCol`, `date`, `dateTime`, `uuid`, `ulid`, `enumCol`, `vector`, the `array*` family, …) lives in the migrations guide.
|
|
1949
|
+
|
|
1950
|
+
## Imperative escape hatch — `extends Migration`
|
|
1951
|
+
|
|
1952
|
+
When a migration is genuinely procedural (a runtime `hasIndex` check, branching on existing schema, interleaving DDL and data), drop to the class form. Here the column types are methods on `this`:
|
|
1953
|
+
|
|
1954
|
+
```ts
|
|
1955
|
+
import { Migration } from "@warlock.js/cascade";
|
|
1956
|
+
|
|
1957
|
+
export default class BackfillStatuses extends Migration {
|
|
1958
|
+
public readonly table = "posts";
|
|
1959
|
+
|
|
1960
|
+
public async up(): Promise<void> {
|
|
1961
|
+
if (!(await this.hasColumn("status"))) {
|
|
1962
|
+
this.string("status").defaultString("draft");
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
this.raw(`UPDATE posts SET status = 'published' WHERE published_at IS NOT NULL`);
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
public down(): void {
|
|
1969
|
+
this.dropColumn("status");
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
```
|
|
1973
|
+
|
|
1974
|
+
Class-form builders include `createTable()` / `createTableIfNotExists()`, `dropTable()` / `dropTableIfExists()`, `dropColumn(name)`, `renameTableTo(name)`, `timestamps()`, `index(...)`, `primaryUuid()`, and `raw(sql)` for a raw statement.
|
|
1975
|
+
|
|
1976
|
+
## Raw SQL migrations
|
|
1977
|
+
|
|
1978
|
+
For SQL-only changes (Postgres), `Migration.rawSql` builds a migration class for you:
|
|
1979
|
+
|
|
1980
|
+
```ts
|
|
1981
|
+
export default Migration.rawSql({
|
|
1982
|
+
name: "2026-01-01-create-auth",
|
|
1983
|
+
up: [`CREATE TABLE sessions (id UUID PRIMARY KEY, user_id UUID REFERENCES users(id))`],
|
|
1984
|
+
down: [`DROP TABLE sessions`],
|
|
1985
|
+
});
|
|
1986
|
+
```
|
|
1987
|
+
|
|
1988
|
+
(Throws on MongoDB — use the declarative or class form there.)
|
|
1989
|
+
|
|
1990
|
+
## Reversibility
|
|
1991
|
+
|
|
1992
|
+
For the class form, `down()` should undo `up()` — the CLI calls it on rollback. (`Migration.create` infers the rollback automatically.) If a migration is genuinely one-way, throw inside `down()` so accidental rollbacks fail loudly:
|
|
1993
|
+
|
|
1994
|
+
```ts
|
|
1995
|
+
public down(): void {
|
|
1996
|
+
throw new Error("This migration is irreversible — restore from backup if you need to undo it.");
|
|
1997
|
+
}
|
|
1998
|
+
```
|
|
1999
|
+
|
|
2000
|
+
## Data-source-aware migrations
|
|
2001
|
+
|
|
2002
|
+
A `Migration.create` / `Migration.alter` migration **inherits its data source from the model** — whatever `static dataSource = "analytics"` the model declares, the migration runs against. You don't pass it in the options.
|
|
2003
|
+
|
|
2004
|
+
```ts
|
|
2005
|
+
// AnalyticsEvent has `static dataSource = "analytics"`, so this migration
|
|
2006
|
+
// targets the analytics database automatically.
|
|
2007
|
+
export default Migration.create(AnalyticsEvent, {
|
|
2008
|
+
type: text().notNullable(),
|
|
2009
|
+
});
|
|
2010
|
+
```
|
|
2011
|
+
|
|
2012
|
+
For a class-form migration not bound to a model, set `public readonly dataSource = "analytics"` directly. See [`@warlock.js/cascade/manage-data-sources/SKILL.md`](@warlock.js/cascade/manage-data-sources/SKILL.md) for the registry.
|
|
2013
|
+
|
|
2014
|
+
## Things NOT to do
|
|
2015
|
+
|
|
2016
|
+
- Don't reach for a `migration({ up(driver) {...} })` factory or `driver.createTable(name, (table) => {...})` — that API doesn't exist. Use `Migration.create(Model, { columns })`, or `extends Migration` with `this.createTable()` for the imperative case.
|
|
2017
|
+
- Don't declare `id` / `createdAt` / `updatedAt` — they're added for you.
|
|
2018
|
+
- Don't auto-run migrations from app code in production. Run them as a deploy step.
|
|
2019
|
+
- Don't put irreversible data backfills in the same file as a schema change — split them so rollback only undoes the schema.
|
|
2020
|
+
- Don't change a committed migration. Add a new one. Editing a migration that already ran in production puts environments out of sync.
|
|
2021
|
+
|
|
2022
|
+
## See also
|
|
2023
|
+
|
|
2024
|
+
- [`@warlock.js/cascade/run-cascade-cli/SKILL.md`](@warlock.js/cascade/run-cascade-cli/SKILL.md) — CLI flags + Operations API for programmatic runs
|
|
2025
|
+
- [`@warlock.js/cascade/manage-data-sources/SKILL.md`](@warlock.js/cascade/manage-data-sources/SKILL.md) — multi-DB migrations
|
|
2026
|
+
|
|
2027
|
+
|