@warlock.js/cascade 4.0.135 → 4.0.137
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/cjs/context/database-transaction-context.d.ts +4 -0
- package/cjs/context/database-transaction-context.d.ts.map +1 -1
- package/cjs/context/database-transaction-context.js +6 -0
- package/cjs/context/database-transaction-context.js.map +1 -1
- package/cjs/contracts/database-driver.contract.d.ts +106 -4
- package/cjs/contracts/database-driver.contract.d.ts.map +1 -1
- package/cjs/contracts/migration-driver.contract.d.ts +49 -1
- package/cjs/contracts/migration-driver.contract.d.ts.map +1 -1
- package/cjs/contracts/query-builder.contract.d.ts +76 -4
- package/cjs/contracts/query-builder.contract.d.ts.map +1 -1
- package/cjs/data-source/data-source.d.ts +40 -1
- package/cjs/data-source/data-source.d.ts.map +1 -1
- package/cjs/data-source/data-source.js +6 -0
- package/cjs/data-source/data-source.js.map +1 -1
- package/cjs/database-dirty-tracker.d.ts +21 -22
- package/cjs/database-dirty-tracker.d.ts.map +1 -1
- package/cjs/database-dirty-tracker.js +14 -17
- package/cjs/database-dirty-tracker.js.map +1 -1
- package/cjs/drivers/mongodb/mongodb-driver.d.ts +29 -1
- package/cjs/drivers/mongodb/mongodb-driver.d.ts.map +1 -1
- package/cjs/drivers/mongodb/mongodb-driver.js +116 -4
- package/cjs/drivers/mongodb/mongodb-driver.js.map +1 -1
- package/cjs/drivers/mongodb/mongodb-migration-driver.d.ts +22 -1
- package/cjs/drivers/mongodb/mongodb-migration-driver.d.ts.map +1 -1
- package/cjs/drivers/mongodb/mongodb-migration-driver.js +27 -0
- package/cjs/drivers/mongodb/mongodb-migration-driver.js.map +1 -1
- package/cjs/drivers/mongodb/mongodb-query-builder.d.ts +41 -9
- package/cjs/drivers/mongodb/mongodb-query-builder.d.ts.map +1 -1
- package/cjs/drivers/mongodb/mongodb-query-builder.js +60 -18
- package/cjs/drivers/mongodb/mongodb-query-builder.js.map +1 -1
- package/cjs/drivers/mongodb/types.d.ts +1 -1
- package/cjs/drivers/mongodb/types.d.ts.map +1 -1
- package/cjs/drivers/postgres/postgres-dialect.d.ts +1 -0
- package/cjs/drivers/postgres/postgres-dialect.d.ts.map +1 -1
- package/cjs/drivers/postgres/postgres-dialect.js +24 -1
- package/cjs/drivers/postgres/postgres-dialect.js.map +1 -1
- package/cjs/drivers/postgres/postgres-driver.d.ts +24 -3
- package/cjs/drivers/postgres/postgres-driver.d.ts.map +1 -1
- package/cjs/drivers/postgres/postgres-driver.js +101 -8
- package/cjs/drivers/postgres/postgres-driver.js.map +1 -1
- package/cjs/drivers/postgres/postgres-migration-driver.d.ts +30 -1
- package/cjs/drivers/postgres/postgres-migration-driver.d.ts.map +1 -1
- package/cjs/drivers/postgres/postgres-migration-driver.js +61 -2
- package/cjs/drivers/postgres/postgres-migration-driver.js.map +1 -1
- package/cjs/drivers/postgres/postgres-query-builder.d.ts +152 -722
- package/cjs/drivers/postgres/postgres-query-builder.d.ts.map +1 -1
- package/cjs/drivers/postgres/postgres-query-builder.js +592 -1459
- package/cjs/drivers/postgres/postgres-query-builder.js.map +1 -1
- package/cjs/drivers/postgres/postgres-query-parser.d.ts +34 -14
- package/cjs/drivers/postgres/postgres-query-parser.d.ts.map +1 -1
- package/cjs/drivers/postgres/postgres-query-parser.js +197 -39
- package/cjs/drivers/postgres/postgres-query-parser.js.map +1 -1
- package/cjs/drivers/sql/sql-dialect.contract.d.ts +1 -0
- package/cjs/drivers/sql/sql-dialect.contract.d.ts.map +1 -1
- package/cjs/errors/transaction-rollback.error.d.ts +20 -0
- package/cjs/errors/transaction-rollback.error.d.ts.map +1 -0
- package/cjs/errors/transaction-rollback.error.js +27 -0
- package/cjs/errors/transaction-rollback.error.js.map +1 -0
- package/cjs/events/model-events.d.ts +1 -1
- package/cjs/events/model-events.d.ts.map +1 -1
- package/cjs/index.d.ts +1 -0
- package/cjs/index.d.ts.map +1 -1
- package/cjs/index.js +1 -1
- package/cjs/migration/column-builder.d.ts +4 -0
- package/cjs/migration/column-builder.d.ts.map +1 -1
- package/cjs/migration/column-builder.js +23 -6
- package/cjs/migration/column-builder.js.map +1 -1
- package/cjs/migration/migration-runner.d.ts.map +1 -1
- package/cjs/migration/migration-runner.js +137 -23
- package/cjs/migration/migration-runner.js.map +1 -1
- package/cjs/migration/migration.d.ts +209 -35
- package/cjs/migration/migration.d.ts.map +1 -1
- package/cjs/migration/migration.js +192 -27
- package/cjs/migration/migration.js.map +1 -1
- package/cjs/model/methods/accessor-methods.d.ts +13 -0
- package/cjs/model/methods/accessor-methods.d.ts.map +1 -0
- package/cjs/model/methods/accessor-methods.js +51 -0
- package/cjs/model/methods/accessor-methods.js.map +1 -0
- package/cjs/model/methods/delete-methods.d.ts +10 -0
- package/cjs/model/methods/delete-methods.d.ts.map +1 -0
- package/cjs/model/methods/delete-methods.js +10 -0
- package/cjs/model/methods/delete-methods.js.map +1 -0
- package/cjs/model/methods/dirty-methods.d.ts +10 -0
- package/cjs/model/methods/dirty-methods.d.ts.map +1 -0
- package/cjs/model/methods/dirty-methods.js +15 -0
- package/cjs/model/methods/dirty-methods.js.map +1 -0
- package/cjs/model/methods/hydration-methods.d.ts +10 -0
- package/cjs/model/methods/hydration-methods.d.ts.map +1 -0
- package/cjs/model/methods/hydration-methods.js +57 -0
- package/cjs/model/methods/hydration-methods.js.map +1 -0
- package/cjs/model/methods/instance-event-methods.d.ts +7 -0
- package/cjs/model/methods/instance-event-methods.d.ts.map +1 -0
- package/cjs/model/methods/instance-event-methods.js +15 -0
- package/cjs/model/methods/instance-event-methods.js.map +1 -0
- package/cjs/model/methods/meta-methods.d.ts +7 -0
- package/cjs/model/methods/meta-methods.d.ts.map +1 -0
- package/cjs/model/methods/meta-methods.js +78 -0
- package/cjs/model/methods/meta-methods.js.map +1 -0
- package/cjs/model/methods/query-methods.d.ts +24 -0
- package/cjs/model/methods/query-methods.d.ts.map +1 -0
- package/cjs/model/methods/query-methods.js +161 -0
- package/cjs/model/methods/query-methods.js.map +1 -0
- package/cjs/model/methods/restore-methods.d.ts +10 -0
- package/cjs/model/methods/restore-methods.d.ts.map +1 -0
- package/cjs/model/methods/restore-methods.js +13 -0
- package/cjs/model/methods/restore-methods.js.map +1 -0
- package/cjs/model/methods/scope-methods.d.ts +7 -0
- package/cjs/model/methods/scope-methods.d.ts.map +1 -0
- package/cjs/model/methods/scope-methods.js +15 -0
- package/cjs/model/methods/scope-methods.js.map +1 -0
- package/cjs/model/methods/serialization-methods.d.ts +3 -0
- package/cjs/model/methods/serialization-methods.d.ts.map +1 -0
- package/cjs/model/methods/serialization-methods.js +27 -0
- package/cjs/model/methods/serialization-methods.js.map +1 -0
- package/cjs/model/methods/static-event-methods.d.ts +9 -0
- package/cjs/model/methods/static-event-methods.d.ts.map +1 -0
- package/cjs/model/methods/static-event-methods.js +29 -0
- package/cjs/model/methods/static-event-methods.js.map +1 -0
- package/cjs/model/methods/write-methods.d.ts +10 -0
- package/cjs/model/methods/write-methods.d.ts.map +1 -0
- package/cjs/model/methods/write-methods.js +52 -0
- package/cjs/model/methods/write-methods.js.map +1 -0
- package/cjs/model/model.d.ts +89 -58
- package/cjs/model/model.d.ts.map +1 -1
- package/cjs/model/model.js +166 -424
- package/cjs/model/model.js.map +1 -1
- package/cjs/model/model.types.d.ts +44 -0
- package/cjs/model/model.types.d.ts.map +1 -0
- package/cjs/model/register-model.d.ts +3 -3
- package/cjs/model/register-model.d.ts.map +1 -1
- package/cjs/query-builder/query-builder.d.ts +556 -0
- package/cjs/query-builder/query-builder.d.ts.map +1 -0
- package/cjs/query-builder/query-builder.js +1070 -0
- package/cjs/query-builder/query-builder.js.map +1 -0
- package/cjs/relations/helpers.d.ts.map +1 -1
- package/cjs/relations/helpers.js +4 -0
- package/cjs/relations/helpers.js.map +1 -1
- package/cjs/relations/index.d.ts +2 -0
- package/cjs/relations/index.d.ts.map +1 -1
- package/cjs/relations/relation-hydrator.d.ts +68 -0
- package/cjs/relations/relation-hydrator.d.ts.map +1 -0
- package/cjs/relations/relation-hydrator.js +81 -0
- package/cjs/relations/relation-hydrator.js.map +1 -0
- package/cjs/relations/relation-loader.js +1 -1
- package/cjs/relations/relation-loader.js.map +1 -1
- package/cjs/relations/types.d.ts +26 -0
- package/cjs/relations/types.d.ts.map +1 -1
- package/cjs/sql-database-dirty-tracker.d.ts +13 -0
- package/cjs/sql-database-dirty-tracker.d.ts.map +1 -0
- package/cjs/sql-database-dirty-tracker.js +14 -0
- package/cjs/sql-database-dirty-tracker.js.map +1 -0
- package/cjs/types.d.ts +59 -0
- package/cjs/types.d.ts.map +1 -1
- package/cjs/utils/connect-to-database.d.ts +50 -1
- package/cjs/utils/connect-to-database.d.ts.map +1 -1
- package/cjs/utils/connect-to-database.js +14 -1
- package/cjs/utils/connect-to-database.js.map +1 -1
- package/cjs/utils/database-writer.utils.d.ts +1 -1
- package/cjs/utils/database-writer.utils.d.ts.map +1 -1
- package/cjs/utils/is-valid-date-value.d.ts +5 -0
- package/cjs/utils/is-valid-date-value.d.ts.map +1 -0
- package/cjs/utils/is-valid-date-value.js +25 -0
- package/cjs/utils/is-valid-date-value.js.map +1 -0
- package/cjs/utils/once-connected.d.ts.map +1 -1
- package/cjs/utils/once-connected.js +4 -5
- package/cjs/utils/once-connected.js.map +1 -1
- package/cjs/writer/database-writer.d.ts.map +1 -1
- package/cjs/writer/database-writer.js +7 -6
- package/cjs/writer/database-writer.js.map +1 -1
- package/esm/context/database-transaction-context.d.ts +4 -0
- package/esm/context/database-transaction-context.d.ts.map +1 -1
- package/esm/context/database-transaction-context.js +6 -0
- package/esm/context/database-transaction-context.js.map +1 -1
- package/esm/contracts/database-driver.contract.d.ts +106 -4
- package/esm/contracts/database-driver.contract.d.ts.map +1 -1
- package/esm/contracts/migration-driver.contract.d.ts +49 -1
- package/esm/contracts/migration-driver.contract.d.ts.map +1 -1
- package/esm/contracts/query-builder.contract.d.ts +76 -4
- package/esm/contracts/query-builder.contract.d.ts.map +1 -1
- package/esm/data-source/data-source.d.ts +40 -1
- package/esm/data-source/data-source.d.ts.map +1 -1
- package/esm/data-source/data-source.js +6 -0
- package/esm/data-source/data-source.js.map +1 -1
- package/esm/database-dirty-tracker.d.ts +21 -22
- package/esm/database-dirty-tracker.d.ts.map +1 -1
- package/esm/database-dirty-tracker.js +14 -17
- package/esm/database-dirty-tracker.js.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.d.ts +29 -1
- package/esm/drivers/mongodb/mongodb-driver.d.ts.map +1 -1
- package/esm/drivers/mongodb/mongodb-driver.js +116 -4
- package/esm/drivers/mongodb/mongodb-driver.js.map +1 -1
- package/esm/drivers/mongodb/mongodb-migration-driver.d.ts +22 -1
- package/esm/drivers/mongodb/mongodb-migration-driver.d.ts.map +1 -1
- package/esm/drivers/mongodb/mongodb-migration-driver.js +27 -0
- package/esm/drivers/mongodb/mongodb-migration-driver.js.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.d.ts +41 -9
- package/esm/drivers/mongodb/mongodb-query-builder.d.ts.map +1 -1
- package/esm/drivers/mongodb/mongodb-query-builder.js +60 -18
- package/esm/drivers/mongodb/mongodb-query-builder.js.map +1 -1
- package/esm/drivers/mongodb/types.d.ts +1 -1
- package/esm/drivers/mongodb/types.d.ts.map +1 -1
- package/esm/drivers/postgres/postgres-dialect.d.ts +1 -0
- package/esm/drivers/postgres/postgres-dialect.d.ts.map +1 -1
- package/esm/drivers/postgres/postgres-dialect.js +24 -1
- package/esm/drivers/postgres/postgres-dialect.js.map +1 -1
- package/esm/drivers/postgres/postgres-driver.d.ts +24 -3
- package/esm/drivers/postgres/postgres-driver.d.ts.map +1 -1
- package/esm/drivers/postgres/postgres-driver.js +101 -8
- package/esm/drivers/postgres/postgres-driver.js.map +1 -1
- package/esm/drivers/postgres/postgres-migration-driver.d.ts +30 -1
- package/esm/drivers/postgres/postgres-migration-driver.d.ts.map +1 -1
- package/esm/drivers/postgres/postgres-migration-driver.js +61 -2
- package/esm/drivers/postgres/postgres-migration-driver.js.map +1 -1
- package/esm/drivers/postgres/postgres-query-builder.d.ts +152 -722
- package/esm/drivers/postgres/postgres-query-builder.d.ts.map +1 -1
- package/esm/drivers/postgres/postgres-query-builder.js +592 -1459
- package/esm/drivers/postgres/postgres-query-builder.js.map +1 -1
- package/esm/drivers/postgres/postgres-query-parser.d.ts +34 -14
- package/esm/drivers/postgres/postgres-query-parser.d.ts.map +1 -1
- package/esm/drivers/postgres/postgres-query-parser.js +197 -39
- package/esm/drivers/postgres/postgres-query-parser.js.map +1 -1
- package/esm/drivers/sql/sql-dialect.contract.d.ts +1 -0
- package/esm/drivers/sql/sql-dialect.contract.d.ts.map +1 -1
- package/esm/errors/transaction-rollback.error.d.ts +20 -0
- package/esm/errors/transaction-rollback.error.d.ts.map +1 -0
- package/esm/errors/transaction-rollback.error.js +27 -0
- package/esm/errors/transaction-rollback.error.js.map +1 -0
- package/esm/events/model-events.d.ts +1 -1
- package/esm/events/model-events.d.ts.map +1 -1
- package/esm/index.d.ts +1 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +1 -1
- package/esm/migration/column-builder.d.ts +4 -0
- package/esm/migration/column-builder.d.ts.map +1 -1
- package/esm/migration/column-builder.js +23 -6
- package/esm/migration/column-builder.js.map +1 -1
- package/esm/migration/migration-runner.d.ts.map +1 -1
- package/esm/migration/migration-runner.js +137 -23
- package/esm/migration/migration-runner.js.map +1 -1
- package/esm/migration/migration.d.ts +209 -35
- package/esm/migration/migration.d.ts.map +1 -1
- package/esm/migration/migration.js +192 -27
- package/esm/migration/migration.js.map +1 -1
- package/esm/model/methods/accessor-methods.d.ts +13 -0
- package/esm/model/methods/accessor-methods.d.ts.map +1 -0
- package/esm/model/methods/accessor-methods.js +51 -0
- package/esm/model/methods/accessor-methods.js.map +1 -0
- package/esm/model/methods/delete-methods.d.ts +10 -0
- package/esm/model/methods/delete-methods.d.ts.map +1 -0
- package/esm/model/methods/delete-methods.js +10 -0
- package/esm/model/methods/delete-methods.js.map +1 -0
- package/esm/model/methods/dirty-methods.d.ts +10 -0
- package/esm/model/methods/dirty-methods.d.ts.map +1 -0
- package/esm/model/methods/dirty-methods.js +15 -0
- package/esm/model/methods/dirty-methods.js.map +1 -0
- package/esm/model/methods/hydration-methods.d.ts +10 -0
- package/esm/model/methods/hydration-methods.d.ts.map +1 -0
- package/esm/model/methods/hydration-methods.js +57 -0
- package/esm/model/methods/hydration-methods.js.map +1 -0
- package/esm/model/methods/instance-event-methods.d.ts +7 -0
- package/esm/model/methods/instance-event-methods.d.ts.map +1 -0
- package/esm/model/methods/instance-event-methods.js +15 -0
- package/esm/model/methods/instance-event-methods.js.map +1 -0
- package/esm/model/methods/meta-methods.d.ts +7 -0
- package/esm/model/methods/meta-methods.d.ts.map +1 -0
- package/esm/model/methods/meta-methods.js +78 -0
- package/esm/model/methods/meta-methods.js.map +1 -0
- package/esm/model/methods/query-methods.d.ts +24 -0
- package/esm/model/methods/query-methods.d.ts.map +1 -0
- package/esm/model/methods/query-methods.js +161 -0
- package/esm/model/methods/query-methods.js.map +1 -0
- package/esm/model/methods/restore-methods.d.ts +10 -0
- package/esm/model/methods/restore-methods.d.ts.map +1 -0
- package/esm/model/methods/restore-methods.js +13 -0
- package/esm/model/methods/restore-methods.js.map +1 -0
- package/esm/model/methods/scope-methods.d.ts +7 -0
- package/esm/model/methods/scope-methods.d.ts.map +1 -0
- package/esm/model/methods/scope-methods.js +15 -0
- package/esm/model/methods/scope-methods.js.map +1 -0
- package/esm/model/methods/serialization-methods.d.ts +3 -0
- package/esm/model/methods/serialization-methods.d.ts.map +1 -0
- package/esm/model/methods/serialization-methods.js +27 -0
- package/esm/model/methods/serialization-methods.js.map +1 -0
- package/esm/model/methods/static-event-methods.d.ts +9 -0
- package/esm/model/methods/static-event-methods.d.ts.map +1 -0
- package/esm/model/methods/static-event-methods.js +29 -0
- package/esm/model/methods/static-event-methods.js.map +1 -0
- package/esm/model/methods/write-methods.d.ts +10 -0
- package/esm/model/methods/write-methods.d.ts.map +1 -0
- package/esm/model/methods/write-methods.js +52 -0
- package/esm/model/methods/write-methods.js.map +1 -0
- package/esm/model/model.d.ts +89 -58
- package/esm/model/model.d.ts.map +1 -1
- package/esm/model/model.js +166 -424
- package/esm/model/model.js.map +1 -1
- package/esm/model/model.types.d.ts +44 -0
- package/esm/model/model.types.d.ts.map +1 -0
- package/esm/model/register-model.d.ts +3 -3
- package/esm/model/register-model.d.ts.map +1 -1
- package/esm/query-builder/query-builder.d.ts +556 -0
- package/esm/query-builder/query-builder.d.ts.map +1 -0
- package/esm/query-builder/query-builder.js +1070 -0
- package/esm/query-builder/query-builder.js.map +1 -0
- package/esm/relations/helpers.d.ts.map +1 -1
- package/esm/relations/helpers.js +4 -0
- package/esm/relations/helpers.js.map +1 -1
- package/esm/relations/index.d.ts +2 -0
- package/esm/relations/index.d.ts.map +1 -1
- package/esm/relations/relation-hydrator.d.ts +68 -0
- package/esm/relations/relation-hydrator.d.ts.map +1 -0
- package/esm/relations/relation-hydrator.js +81 -0
- package/esm/relations/relation-hydrator.js.map +1 -0
- package/esm/relations/relation-loader.js +1 -1
- package/esm/relations/relation-loader.js.map +1 -1
- package/esm/relations/types.d.ts +26 -0
- package/esm/relations/types.d.ts.map +1 -1
- package/esm/sql-database-dirty-tracker.d.ts +13 -0
- package/esm/sql-database-dirty-tracker.d.ts.map +1 -0
- package/esm/sql-database-dirty-tracker.js +14 -0
- package/esm/sql-database-dirty-tracker.js.map +1 -0
- package/esm/types.d.ts +59 -0
- package/esm/types.d.ts.map +1 -1
- package/esm/utils/connect-to-database.d.ts +50 -1
- package/esm/utils/connect-to-database.d.ts.map +1 -1
- package/esm/utils/connect-to-database.js +14 -1
- package/esm/utils/connect-to-database.js.map +1 -1
- package/esm/utils/database-writer.utils.d.ts +1 -1
- package/esm/utils/database-writer.utils.d.ts.map +1 -1
- package/esm/utils/is-valid-date-value.d.ts +5 -0
- package/esm/utils/is-valid-date-value.d.ts.map +1 -0
- package/esm/utils/is-valid-date-value.js +25 -0
- package/esm/utils/is-valid-date-value.js.map +1 -0
- package/esm/utils/once-connected.d.ts.map +1 -1
- package/esm/utils/once-connected.js +4 -5
- package/esm/utils/once-connected.js.map +1 -1
- package/esm/writer/database-writer.d.ts.map +1 -1
- package/esm/writer/database-writer.js +7 -6
- package/esm/writer/database-writer.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,155 +1,193 @@
|
|
|
1
|
-
'use strict';var dataSourceRegistry=require('../../data-source/data-source-registry.js'),registerModel=require('../../model/register-model.js'),postgresQueryParser=require('./postgres-query-parser.js');/**
|
|
1
|
+
'use strict';var dataSourceRegistry=require('../../data-source/data-source-registry.js'),registerModel=require('../../model/register-model.js'),queryBuilder=require('../../query-builder/query-builder.js'),postgresQueryParser=require('./postgres-query-parser.js');/**
|
|
2
2
|
* PostgreSQL Query Builder
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* parameter handling and type safety.
|
|
4
|
+
* Extends the pure QueryBuilder base with PostgreSQL-specific execution,
|
|
5
|
+
* SQL generation, relation hydration, and scope management.
|
|
7
6
|
*
|
|
8
7
|
* @module cascade/drivers/postgres
|
|
9
8
|
*/
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// HELPER
|
|
11
|
+
// ============================================================================
|
|
12
|
+
/**
|
|
13
|
+
* Cast an Op[] to PostgresParserOperation[] — the shapes are compatible since
|
|
14
|
+
* both have `type: string` and `data: Record<string, unknown>`.
|
|
15
|
+
*/
|
|
16
|
+
function toParserOps(ops) {
|
|
17
|
+
return ops;
|
|
18
|
+
}
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// POSTGRES QUERY BUILDER
|
|
21
|
+
// ============================================================================
|
|
10
22
|
/**
|
|
11
23
|
* PostgreSQL Query Builder.
|
|
12
24
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* for SQL generation.
|
|
25
|
+
* Collects query operations (via the base class) and delegates SQL generation
|
|
26
|
+
* to `PostgresQueryParser`. Owns execution, hydration, and relation loading.
|
|
16
27
|
*
|
|
17
28
|
* @example
|
|
18
29
|
* ```typescript
|
|
19
|
-
* const users = await
|
|
20
|
-
* .select([
|
|
21
|
-
* .where(
|
|
22
|
-
* .
|
|
23
|
-
* .orderBy('createdAt', 'desc')
|
|
30
|
+
* const users = await User.query()
|
|
31
|
+
* .select(["id", "name", "email"])
|
|
32
|
+
* .where("status", "active")
|
|
33
|
+
* .orderBy("createdAt", "desc")
|
|
24
34
|
* .limit(10)
|
|
25
35
|
* .get();
|
|
26
36
|
* ```
|
|
27
37
|
*/
|
|
28
|
-
class PostgresQueryBuilder {
|
|
38
|
+
class PostgresQueryBuilder extends queryBuilder.QueryBuilder {
|
|
29
39
|
table;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Data source instance.
|
|
36
|
-
*/
|
|
40
|
+
// ──────────────────────────────────────────────────────────────
|
|
41
|
+
// POSTGRES-SPECIFIC STATE
|
|
42
|
+
// ──────────────────────────────────────────────────────────────
|
|
43
|
+
/** Data source backing this builder. */
|
|
37
44
|
dataSource;
|
|
38
|
-
/**
|
|
39
|
-
* Hydrate callback for transforming results.
|
|
40
|
-
*/
|
|
45
|
+
/** Hydration callback for transforming result rows into model instances. */
|
|
41
46
|
hydrateCallback;
|
|
42
|
-
/**
|
|
43
|
-
* Callback invoked before query execution.
|
|
44
|
-
*/
|
|
47
|
+
/** Invoked before query execution. */
|
|
45
48
|
fetchingCallback;
|
|
46
|
-
/**
|
|
47
|
-
* Callback invoked after records fetched but before hydration.
|
|
48
|
-
*/
|
|
49
|
+
/** Invoked after fetch but before hydration. */
|
|
49
50
|
hydratingCallback;
|
|
50
|
-
/**
|
|
51
|
-
* Callback invoked after records fetched and hydrated.
|
|
52
|
-
*/
|
|
51
|
+
/** Invoked after fetch and hydration. */
|
|
53
52
|
fetchedCallback;
|
|
54
53
|
/**
|
|
55
|
-
*
|
|
56
|
-
|
|
57
|
-
pendingGlobalScopes;
|
|
58
|
-
/**
|
|
59
|
-
* Available local scopes.
|
|
54
|
+
* Map of relations registered via `joinWith()`.
|
|
55
|
+
* Keyed by dot-notation path (e.g. "organizationAiModel.aiModel").
|
|
60
56
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
disabledGlobalScopes = new Set();
|
|
66
|
-
/**
|
|
67
|
-
* Whether scopes have been applied.
|
|
68
|
-
*/
|
|
69
|
-
scopesApplied = false;
|
|
57
|
+
joinRelations = new Map();
|
|
58
|
+
// ──────────────────────────────────────────────────────────────
|
|
59
|
+
// CONSTRUCTOR
|
|
60
|
+
// ──────────────────────────────────────────────────────────────
|
|
70
61
|
/**
|
|
71
|
-
* Create a new query builder.
|
|
72
|
-
*
|
|
73
62
|
* @param table - Target table name
|
|
74
|
-
* @param dataSource - Optional data source
|
|
63
|
+
* @param dataSource - Optional (uses default data source from registry if omitted)
|
|
75
64
|
*/
|
|
76
65
|
constructor(table, dataSource) {
|
|
66
|
+
super();
|
|
77
67
|
this.table = table;
|
|
78
68
|
this.dataSource = dataSource ?? dataSourceRegistry.dataSourceRegistry.get();
|
|
79
69
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
70
|
+
// ──────────────────────────────────────────────────────────────
|
|
71
|
+
// DRIVER
|
|
72
|
+
// ──────────────────────────────────────────────────────────────
|
|
83
73
|
get driver() {
|
|
84
74
|
return this.dataSource.driver;
|
|
85
75
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
* @param type - Operation type
|
|
90
|
-
* @param data - Operation data
|
|
91
|
-
*/
|
|
92
|
-
addOperation(type, data) {
|
|
93
|
-
this.operations.push({ type, data });
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Clone this query builder with all current operations.
|
|
97
|
-
*
|
|
98
|
-
* @returns New query builder instance
|
|
99
|
-
*/
|
|
76
|
+
// ──────────────────────────────────────────────────────────────
|
|
77
|
+
// CLONE
|
|
78
|
+
// ──────────────────────────────────────────────────────────────
|
|
100
79
|
clone() {
|
|
101
80
|
const cloned = new PostgresQueryBuilder(this.table, this.dataSource);
|
|
81
|
+
// Copy base-class state
|
|
102
82
|
cloned.operations = [...this.operations];
|
|
103
|
-
cloned.hydrateCallback = this.hydrateCallback;
|
|
104
83
|
cloned.pendingGlobalScopes = this.pendingGlobalScopes;
|
|
105
84
|
cloned.availableLocalScopes = this.availableLocalScopes;
|
|
106
85
|
cloned.disabledGlobalScopes = new Set(this.disabledGlobalScopes);
|
|
107
86
|
cloned.scopesApplied = this.scopesApplied;
|
|
87
|
+
cloned.eagerLoadRelations = new Map(this.eagerLoadRelations);
|
|
88
|
+
cloned.countRelations = [...this.countRelations];
|
|
89
|
+
cloned.relationDefinitions = this.relationDefinitions;
|
|
90
|
+
cloned.modelClass = this.modelClass;
|
|
91
|
+
// Copy PG-specific state
|
|
92
|
+
cloned.hydrateCallback = this.hydrateCallback;
|
|
93
|
+
cloned.joinRelations = new Map(this.joinRelations);
|
|
108
94
|
return cloned;
|
|
109
95
|
}
|
|
110
96
|
// ============================================================================
|
|
111
|
-
//
|
|
97
|
+
// PG-SPECIFIC FLUENT METHODS
|
|
112
98
|
// ============================================================================
|
|
113
99
|
/**
|
|
114
|
-
*
|
|
100
|
+
* Native-query escape hatch. Passes `operations[]` to the callback for
|
|
101
|
+
* direct manipulation. Use sparingly — only when fluent API is insufficient.
|
|
115
102
|
*
|
|
116
|
-
* @
|
|
117
|
-
*
|
|
103
|
+
* @example
|
|
104
|
+
* q.raw(ops => ops.push({ type: "whereRaw", data: { expression: "1=1" } }))
|
|
118
105
|
*/
|
|
119
|
-
|
|
120
|
-
this.
|
|
106
|
+
raw(callback) {
|
|
107
|
+
callback(this.operations);
|
|
121
108
|
return this;
|
|
122
109
|
}
|
|
123
110
|
/**
|
|
124
|
-
*
|
|
111
|
+
* Record a DISTINCT flag AND auto-select the field(s).
|
|
112
|
+
* In PostgreSQL, DISTINCT ON (col) requires the col to appear in SELECT.
|
|
125
113
|
*
|
|
126
|
-
* @
|
|
127
|
-
*
|
|
114
|
+
* @example
|
|
115
|
+
* q.distinctValues("category") // SELECT category … DISTINCT ON (category)
|
|
116
|
+
* q.distinctValues(["category", "status"]) // both fields in DISTINCT ON and SELECT
|
|
128
117
|
*/
|
|
118
|
+
distinctValues(fields) {
|
|
119
|
+
// Record the base DISTINCT flag op
|
|
120
|
+
super.distinctValues(fields);
|
|
121
|
+
// Also add a select for the field(s) so they appear in the SELECT clause
|
|
122
|
+
if (fields) {
|
|
123
|
+
const fieldArr = Array.isArray(fields) ? fields : [fields];
|
|
124
|
+
this.addOperation("select", { fields: fieldArr });
|
|
125
|
+
}
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Nearest-neighbour vector similarity search via pgvector cosine distance.
|
|
130
|
+
*
|
|
131
|
+
* Adds two operations atomically:
|
|
132
|
+
* 1. `selectRaw` → `1 - (column <=> $n::vector) AS <alias>`
|
|
133
|
+
* Makes the similarity score available on every returned row.
|
|
134
|
+
* 2. `orderByRaw` → `column <=> $n::vector`
|
|
135
|
+
* Tells the PostgreSQL query planner to use the IVFFlat/HNSW vector index.
|
|
136
|
+
* Using the alias in ORDER BY would bypass the index — the raw expression is required.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```typescript
|
|
140
|
+
* const results = await Vector.query()
|
|
141
|
+
* .where({ organization_id: "org-123", content_type: "summary" })
|
|
142
|
+
* .nearestTo("embedding", queryEmbedding)
|
|
143
|
+
* .limit(5)
|
|
144
|
+
* .get<VectorRow & { score: number }>();
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
nearestTo(column, embedding, alias = "score") {
|
|
148
|
+
// pgvector expects the literal format: [n,n,n,...]
|
|
149
|
+
const literal = `[${embedding.join(",")}]`;
|
|
150
|
+
const quotedCol = this.driver.dialect.quoteIdentifier(column);
|
|
151
|
+
const quotedTable = this.driver.dialect.quoteIdentifier(this.table);
|
|
152
|
+
// 0 — Preserve all table columns.
|
|
153
|
+
// Adding a selectRaw suppresses the parser's "SELECT *" fallback,
|
|
154
|
+
// so we must explicitly include table.* before the score expression.
|
|
155
|
+
this.addOperation("selectRaw", {
|
|
156
|
+
expression: `${quotedTable}.*`,
|
|
157
|
+
bindings: [],
|
|
158
|
+
});
|
|
159
|
+
// 1 — Add similarity score to SELECT
|
|
160
|
+
this.addOperation("selectRaw", {
|
|
161
|
+
expression: `1 - (${quotedCol} <=> ?::vector) AS ${alias}`,
|
|
162
|
+
bindings: [literal],
|
|
163
|
+
});
|
|
164
|
+
// 2 — ORDER BY the raw expression so the vector index is used
|
|
165
|
+
this.addOperation("orderByRaw", {
|
|
166
|
+
expression: `${quotedCol} <=> ?::vector`,
|
|
167
|
+
bindings: [literal],
|
|
168
|
+
});
|
|
169
|
+
return this;
|
|
170
|
+
}
|
|
171
|
+
/** Set a hydration callback that transforms each result row. */
|
|
172
|
+
hydrate(callback) {
|
|
173
|
+
this.hydrateCallback = callback;
|
|
174
|
+
return this;
|
|
175
|
+
}
|
|
176
|
+
/** Register a callback invoked before query execution. */
|
|
129
177
|
onFetching(callback) {
|
|
130
178
|
this.fetchingCallback = callback;
|
|
131
179
|
return () => {
|
|
132
180
|
this.fetchingCallback = undefined;
|
|
133
181
|
};
|
|
134
182
|
}
|
|
135
|
-
/**
|
|
136
|
-
* Register callback invoked after fetch but before hydration.
|
|
137
|
-
*
|
|
138
|
-
* @param callback - Callback function
|
|
139
|
-
* @returns Unsubscribe function
|
|
140
|
-
*/
|
|
183
|
+
/** Register a callback invoked after fetch but before hydration. */
|
|
141
184
|
onHydrating(callback) {
|
|
142
185
|
this.hydratingCallback = callback;
|
|
143
186
|
return () => {
|
|
144
187
|
this.hydratingCallback = undefined;
|
|
145
188
|
};
|
|
146
189
|
}
|
|
147
|
-
/**
|
|
148
|
-
* Register callback invoked after fetch and hydration.
|
|
149
|
-
*
|
|
150
|
-
* @param callback - Callback function
|
|
151
|
-
* @returns Unsubscribe function
|
|
152
|
-
*/
|
|
190
|
+
/** Register a callback invoked after fetch and hydration. */
|
|
153
191
|
onFetched(callback) {
|
|
154
192
|
this.fetchedCallback = callback;
|
|
155
193
|
return () => {
|
|
@@ -159,1300 +197,407 @@ class PostgresQueryBuilder {
|
|
|
159
197
|
// ============================================================================
|
|
160
198
|
// SCOPES
|
|
161
199
|
// ============================================================================
|
|
162
|
-
/**
|
|
163
|
-
* Disable one or more global scopes for this query.
|
|
164
|
-
*
|
|
165
|
-
* @param scopeNames - Scope names to disable
|
|
166
|
-
* @returns This builder for chaining
|
|
167
|
-
*/
|
|
168
|
-
withoutGlobalScope(...scopeNames) {
|
|
169
|
-
scopeNames.forEach((name) => this.disabledGlobalScopes.add(name));
|
|
170
|
-
return this;
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Disable all global scopes for this query.
|
|
174
|
-
*
|
|
175
|
-
* @returns This builder for chaining
|
|
176
|
-
*/
|
|
177
|
-
withoutGlobalScopes() {
|
|
178
|
-
if (this.pendingGlobalScopes) {
|
|
179
|
-
this.pendingGlobalScopes.forEach((_, name) => {
|
|
180
|
-
this.disabledGlobalScopes.add(name);
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
return this;
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Apply a local scope to this query.
|
|
187
|
-
*
|
|
188
|
-
* @param scopeName - Name of the local scope
|
|
189
|
-
* @returns This builder for chaining
|
|
190
|
-
*/
|
|
191
|
-
scope(scopeName) {
|
|
192
|
-
if (!this.availableLocalScopes) {
|
|
193
|
-
throw new Error("No local scopes available");
|
|
194
|
-
}
|
|
195
|
-
const scopeCallback = this.availableLocalScopes.get(scopeName);
|
|
196
|
-
if (!scopeCallback) {
|
|
197
|
-
throw new Error(`Local scope "${scopeName}" not found`);
|
|
198
|
-
}
|
|
199
|
-
scopeCallback(this);
|
|
200
|
-
return this;
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Apply pending global scopes before query execution.
|
|
204
|
-
*/
|
|
200
|
+
/** Apply pending global scopes to the operations list. */
|
|
205
201
|
applyPendingScopes() {
|
|
206
|
-
if (!this.pendingGlobalScopes || this.scopesApplied)
|
|
202
|
+
if (!this.pendingGlobalScopes || this.scopesApplied)
|
|
207
203
|
return;
|
|
208
|
-
}
|
|
209
204
|
const beforeOps = [];
|
|
210
205
|
const afterOps = [];
|
|
211
206
|
for (const [name, { callback, timing }] of this.pendingGlobalScopes) {
|
|
212
|
-
if (this.disabledGlobalScopes.has(name))
|
|
207
|
+
if (this.disabledGlobalScopes.has(name))
|
|
213
208
|
continue;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
callback(tempBuilder);
|
|
209
|
+
const temp = new PostgresQueryBuilder(this.table, this.dataSource);
|
|
210
|
+
callback(temp);
|
|
217
211
|
if (timing === "before") {
|
|
218
|
-
beforeOps.push(...
|
|
212
|
+
beforeOps.push(...temp.operations);
|
|
219
213
|
}
|
|
220
214
|
else {
|
|
221
|
-
afterOps.push(...
|
|
215
|
+
afterOps.push(...temp.operations);
|
|
222
216
|
}
|
|
223
217
|
}
|
|
224
218
|
this.operations = [...beforeOps, ...this.operations, ...afterOps];
|
|
225
219
|
this.scopesApplied = true;
|
|
226
220
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
for (const [key, value] of Object.entries(args[0])) {
|
|
239
|
-
this.addOperation("where", { field: key, operator: "=", value });
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
else if (args.length === 2) {
|
|
244
|
-
this.addOperation("where", { field: args[0], operator: "=", value: args[1] });
|
|
221
|
+
// ============================================================================
|
|
222
|
+
// WHERE — POSTGRES-SPECIFIC (driver.dialect required)
|
|
223
|
+
// ============================================================================
|
|
224
|
+
/** Array field contains a value (or object with key). */
|
|
225
|
+
whereArrayContains(field, value, key) {
|
|
226
|
+
const quotedField = this.driver.dialect.quoteIdentifier(field);
|
|
227
|
+
if (key) {
|
|
228
|
+
this.addOperation("whereRaw", {
|
|
229
|
+
expression: `${quotedField} @> ?::jsonb`,
|
|
230
|
+
bindings: [JSON.stringify([{ [key]: value }])],
|
|
231
|
+
});
|
|
245
232
|
}
|
|
246
|
-
else
|
|
247
|
-
this.addOperation("
|
|
233
|
+
else {
|
|
234
|
+
this.addOperation("whereRaw", {
|
|
235
|
+
expression: `? = ANY(${quotedField})`,
|
|
236
|
+
bindings: [value],
|
|
237
|
+
});
|
|
248
238
|
}
|
|
249
239
|
return this;
|
|
250
240
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
241
|
+
/** Array field does NOT contain a value (or object with key). */
|
|
242
|
+
whereArrayNotContains(field, value, key) {
|
|
243
|
+
const quotedField = this.driver.dialect.quoteIdentifier(field);
|
|
244
|
+
if (key) {
|
|
245
|
+
this.addOperation("whereRaw", {
|
|
246
|
+
expression: `NOT (${quotedField} @> ?::jsonb)`,
|
|
247
|
+
bindings: [JSON.stringify([{ [key]: value }])],
|
|
248
|
+
});
|
|
254
249
|
}
|
|
255
|
-
else
|
|
256
|
-
this.addOperation("
|
|
250
|
+
else {
|
|
251
|
+
this.addOperation("whereRaw", {
|
|
252
|
+
expression: `NOT (? = ANY(${quotedField}))`,
|
|
253
|
+
bindings: [value],
|
|
254
|
+
});
|
|
257
255
|
}
|
|
258
256
|
return this;
|
|
259
257
|
}
|
|
260
|
-
/**
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// ============================================================================
|
|
275
|
-
// WHERE CLAUSES - COLUMN COMPARISONS
|
|
276
|
-
// ============================================================================
|
|
277
|
-
/**
|
|
278
|
-
* Compare two columns.
|
|
279
|
-
*/
|
|
280
|
-
whereColumn(first, operator, second) {
|
|
281
|
-
this.addOperation("whereColumn", { first, operator, second });
|
|
282
|
-
return this;
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Compare two columns with OR.
|
|
286
|
-
*/
|
|
287
|
-
orWhereColumn(first, operator, second) {
|
|
288
|
-
this.addOperation("orWhereColumn", { first, operator, second });
|
|
289
|
-
return this;
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Compare multiple column pairs.
|
|
293
|
-
*/
|
|
294
|
-
whereColumns(comparisons) {
|
|
295
|
-
for (const [left, operator, right] of comparisons) {
|
|
296
|
-
this.whereColumn(left, operator, right);
|
|
258
|
+
/** Array field contains value OR is empty. */
|
|
259
|
+
whereArrayHasOrEmpty(field, value, key) {
|
|
260
|
+
const quotedField = this.driver.dialect.quoteIdentifier(field);
|
|
261
|
+
if (key) {
|
|
262
|
+
this.addOperation("whereRaw", {
|
|
263
|
+
expression: `(${quotedField} @> ?::jsonb OR ${quotedField} = '[]'::jsonb OR ${quotedField} IS NULL)`,
|
|
264
|
+
bindings: [JSON.stringify([{ [key]: value }])],
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
this.addOperation("whereRaw", {
|
|
269
|
+
expression: `(? = ANY(${quotedField}) OR array_length(${quotedField}, 1) IS NULL)`,
|
|
270
|
+
bindings: [value],
|
|
271
|
+
});
|
|
297
272
|
}
|
|
298
273
|
return this;
|
|
299
274
|
}
|
|
300
|
-
/**
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return this;
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Alias for whereDate.
|
|
319
|
-
*/
|
|
320
|
-
whereDateEquals(field, value) {
|
|
321
|
-
return this.whereDate(field, value);
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Filter for dates before a value.
|
|
325
|
-
*/
|
|
326
|
-
whereDateBefore(field, value) {
|
|
327
|
-
this.addOperation("whereDateBefore", { field, value });
|
|
328
|
-
return this;
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Filter for dates after a value.
|
|
332
|
-
*/
|
|
333
|
-
whereDateAfter(field, value) {
|
|
334
|
-
this.addOperation("whereDateAfter", { field, value });
|
|
335
|
-
return this;
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* Filter by time.
|
|
339
|
-
*/
|
|
340
|
-
whereTime(field, value) {
|
|
341
|
-
this.addOperation("where", { field, operator: "=", value, timeOnly: true });
|
|
342
|
-
return this;
|
|
343
|
-
}
|
|
344
|
-
/**
|
|
345
|
-
* Filter by day of month.
|
|
346
|
-
*/
|
|
347
|
-
whereDay(field, value) {
|
|
348
|
-
this.addOperation("whereRaw", {
|
|
349
|
-
expression: `EXTRACT(DAY FROM ${field}) = ?`,
|
|
350
|
-
bindings: [value],
|
|
351
|
-
});
|
|
352
|
-
return this;
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* Filter by month.
|
|
356
|
-
*/
|
|
357
|
-
whereMonth(field, value) {
|
|
358
|
-
this.addOperation("whereRaw", {
|
|
359
|
-
expression: `EXTRACT(MONTH FROM ${field}) = ?`,
|
|
360
|
-
bindings: [value],
|
|
361
|
-
});
|
|
362
|
-
return this;
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Filter by year.
|
|
366
|
-
*/
|
|
367
|
-
whereYear(field, value) {
|
|
368
|
-
this.addOperation("whereRaw", {
|
|
369
|
-
expression: `EXTRACT(YEAR FROM ${field}) = ?`,
|
|
370
|
-
bindings: [value],
|
|
371
|
-
});
|
|
372
|
-
return this;
|
|
373
|
-
}
|
|
374
|
-
// ============================================================================
|
|
375
|
-
// WHERE CLAUSES - JSON OPERATIONS
|
|
376
|
-
// ============================================================================
|
|
377
|
-
/**
|
|
378
|
-
* Check if JSON contains value.
|
|
379
|
-
*/
|
|
380
|
-
whereJsonContains(path, value) {
|
|
381
|
-
this.addOperation("whereJsonContains", { path, value });
|
|
382
|
-
return this;
|
|
383
|
-
}
|
|
384
|
-
/**
|
|
385
|
-
* Check if JSON doesn't contain value.
|
|
386
|
-
*/
|
|
387
|
-
whereJsonDoesntContain(path, value) {
|
|
388
|
-
this.addOperation("whereJsonDoesntContain", { path, value });
|
|
389
|
-
return this;
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Check if JSON contains key.
|
|
393
|
-
*/
|
|
394
|
-
whereJsonContainsKey(path) {
|
|
395
|
-
this.addOperation("whereRaw", {
|
|
396
|
-
expression: `${path} IS NOT NULL`,
|
|
397
|
-
bindings: [],
|
|
398
|
-
});
|
|
399
|
-
return this;
|
|
400
|
-
}
|
|
401
|
-
/**
|
|
402
|
-
* Check JSON array/string length.
|
|
403
|
-
*/
|
|
404
|
-
whereJsonLength(path, operator, value) {
|
|
405
|
-
this.addOperation("whereRaw", {
|
|
406
|
-
expression: `jsonb_array_length(${path}) ${operator} ?`,
|
|
407
|
-
bindings: [value],
|
|
408
|
-
});
|
|
409
|
-
return this;
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* Check if JSON is array.
|
|
413
|
-
*/
|
|
414
|
-
whereJsonIsArray(path) {
|
|
415
|
-
this.addOperation("whereRaw", {
|
|
416
|
-
expression: `jsonb_typeof(${path}) = 'array'`,
|
|
417
|
-
bindings: [],
|
|
418
|
-
});
|
|
419
|
-
return this;
|
|
420
|
-
}
|
|
421
|
-
/**
|
|
422
|
-
* Check if JSON is object.
|
|
423
|
-
*/
|
|
424
|
-
whereJsonIsObject(path) {
|
|
425
|
-
this.addOperation("whereRaw", {
|
|
426
|
-
expression: `jsonb_typeof(${path}) = 'object'`,
|
|
427
|
-
bindings: [],
|
|
428
|
-
});
|
|
429
|
-
return this;
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Check array length.
|
|
433
|
-
*/
|
|
434
|
-
whereArrayLength(field, operator, value) {
|
|
435
|
-
this.addOperation("whereRaw", {
|
|
436
|
-
expression: `array_length(${field}, 1) ${operator} ?`,
|
|
437
|
-
bindings: [value],
|
|
438
|
-
});
|
|
275
|
+
/** Array field does NOT contain value OR is empty. */
|
|
276
|
+
whereArrayNotHaveOrEmpty(field, value, key) {
|
|
277
|
+
const quotedField = this.driver.dialect.quoteIdentifier(field);
|
|
278
|
+
if (key) {
|
|
279
|
+
this.addOperation("whereRaw", {
|
|
280
|
+
expression: `(NOT (${quotedField} @> ?::jsonb) OR ${quotedField} = '[]'::jsonb OR ${quotedField} IS NULL)`,
|
|
281
|
+
bindings: [JSON.stringify([{ [key]: value }])],
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
this.addOperation("whereRaw", {
|
|
286
|
+
expression: `(NOT (? = ANY(${quotedField})) OR array_length(${quotedField}, 1) IS NULL)`,
|
|
287
|
+
bindings: [value],
|
|
288
|
+
});
|
|
289
|
+
}
|
|
439
290
|
return this;
|
|
440
291
|
}
|
|
441
292
|
// ============================================================================
|
|
442
|
-
//
|
|
293
|
+
// joinWith — RESOLVE RELATION DEFINITIONS
|
|
443
294
|
// ============================================================================
|
|
444
295
|
/**
|
|
445
|
-
*
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
*
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
* Filter by ULID.
|
|
464
|
-
*/
|
|
465
|
-
whereUlid(value) {
|
|
466
|
-
return this.where("ulid", value);
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* Full-text search.
|
|
470
|
-
*/
|
|
471
|
-
whereFullText(fields, query) {
|
|
472
|
-
const fieldList = Array.isArray(fields) ? fields : [fields];
|
|
473
|
-
this.addOperation("whereFullText", { fields: fieldList, query });
|
|
474
|
-
return this;
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* Full-text search with OR.
|
|
478
|
-
*/
|
|
479
|
-
orWhereFullText(fields, query) {
|
|
480
|
-
// TODO: Handle OR full-text
|
|
481
|
-
return this.whereFullText(fields, query);
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* Alias for whereFullText.
|
|
485
|
-
*/
|
|
486
|
-
whereSearch(field, query) {
|
|
487
|
-
return this.whereFullText(field, query);
|
|
488
|
-
}
|
|
489
|
-
/**
|
|
490
|
-
* Negate conditions.
|
|
491
|
-
*/
|
|
492
|
-
whereNot(callback) {
|
|
493
|
-
// TODO: Implement NOT wrapper
|
|
494
|
-
return this;
|
|
495
|
-
}
|
|
496
|
-
/**
|
|
497
|
-
* Negate conditions with OR.
|
|
498
|
-
*/
|
|
499
|
-
orWhereNot(callback) {
|
|
500
|
-
// TODO: Implement OR NOT wrapper
|
|
501
|
-
return this;
|
|
502
|
-
}
|
|
503
|
-
// ============================================================================
|
|
504
|
-
// WHERE CLAUSES - COMPARISON OPERATORS
|
|
505
|
-
// ============================================================================
|
|
506
|
-
/**
|
|
507
|
-
* Filter by value in array.
|
|
508
|
-
*/
|
|
509
|
-
whereIn(field, values) {
|
|
510
|
-
this.addOperation("whereIn", { field, values });
|
|
511
|
-
return this;
|
|
512
|
-
}
|
|
513
|
-
/**
|
|
514
|
-
* Filter by value not in array.
|
|
515
|
-
*/
|
|
516
|
-
whereNotIn(field, values) {
|
|
517
|
-
this.addOperation("whereNotIn", { field, values });
|
|
518
|
-
return this;
|
|
519
|
-
}
|
|
520
|
-
/**
|
|
521
|
-
* Filter by NULL value.
|
|
522
|
-
*/
|
|
523
|
-
whereNull(field) {
|
|
524
|
-
this.addOperation("whereNull", { field });
|
|
525
|
-
return this;
|
|
526
|
-
}
|
|
527
|
-
/**
|
|
528
|
-
* Filter by NOT NULL value.
|
|
529
|
-
*/
|
|
530
|
-
whereNotNull(field) {
|
|
531
|
-
this.addOperation("whereNotNull", { field });
|
|
532
|
-
return this;
|
|
533
|
-
}
|
|
534
|
-
/**
|
|
535
|
-
* Filter by range (inclusive).
|
|
536
|
-
*/
|
|
537
|
-
whereBetween(field, range) {
|
|
538
|
-
this.addOperation("whereBetween", { field, range });
|
|
539
|
-
return this;
|
|
540
|
-
}
|
|
541
|
-
/**
|
|
542
|
-
* Filter by not in range.
|
|
543
|
-
*/
|
|
544
|
-
whereNotBetween(field, range) {
|
|
545
|
-
this.addOperation("whereNotBetween", { field, range });
|
|
546
|
-
return this;
|
|
547
|
-
}
|
|
548
|
-
// ============================================================================
|
|
549
|
-
// WHERE CLAUSES - PATTERN MATCHING
|
|
550
|
-
// ============================================================================
|
|
551
|
-
/**
|
|
552
|
-
* Filter by LIKE pattern (case-insensitive).
|
|
553
|
-
*/
|
|
554
|
-
whereLike(field, pattern) {
|
|
555
|
-
const patternStr = pattern instanceof RegExp ? pattern.source : pattern;
|
|
556
|
-
this.addOperation("whereLike", { field, pattern: patternStr });
|
|
557
|
-
return this;
|
|
558
|
-
}
|
|
559
|
-
/**
|
|
560
|
-
* Filter by NOT LIKE pattern.
|
|
561
|
-
*/
|
|
562
|
-
whereNotLike(field, pattern) {
|
|
563
|
-
const patternStr = pattern instanceof RegExp ? pattern.source : pattern;
|
|
564
|
-
this.addOperation("whereNotLike", { field, pattern: patternStr });
|
|
565
|
-
return this;
|
|
566
|
-
}
|
|
567
|
-
/**
|
|
568
|
-
* Filter by prefix.
|
|
569
|
-
*/
|
|
570
|
-
whereStartsWith(field, value) {
|
|
571
|
-
return this.whereLike(field, `${value}%`);
|
|
572
|
-
}
|
|
573
|
-
/**
|
|
574
|
-
* Filter by not starting with prefix.
|
|
575
|
-
*/
|
|
576
|
-
whereNotStartsWith(field, value) {
|
|
577
|
-
return this.whereNotLike(field, `${value}%`);
|
|
578
|
-
}
|
|
579
|
-
/**
|
|
580
|
-
* Filter by suffix.
|
|
581
|
-
*/
|
|
582
|
-
whereEndsWith(field, value) {
|
|
583
|
-
return this.whereLike(field, `%${value}`);
|
|
584
|
-
}
|
|
585
|
-
/**
|
|
586
|
-
* Filter by not ending with suffix.
|
|
587
|
-
*/
|
|
588
|
-
whereNotEndsWith(field, value) {
|
|
589
|
-
return this.whereNotLike(field, `%${value}`);
|
|
590
|
-
}
|
|
591
|
-
/**
|
|
592
|
-
* Filter by date range.
|
|
593
|
-
*/
|
|
594
|
-
whereDateBetween(field, range) {
|
|
595
|
-
this.addOperation("whereDateBetween", { field, range });
|
|
596
|
-
return this;
|
|
597
|
-
}
|
|
598
|
-
/**
|
|
599
|
-
* Filter by not in date range.
|
|
600
|
-
*/
|
|
601
|
-
whereDateNotBetween(field, range) {
|
|
602
|
-
// Use NOT BETWEEN
|
|
603
|
-
this.addOperation("whereNotBetween", { field, range });
|
|
604
|
-
return this;
|
|
605
|
-
}
|
|
606
|
-
whereExists(param) {
|
|
607
|
-
if (typeof param === "function") {
|
|
608
|
-
// Subquery exists
|
|
609
|
-
const tempBuilder = new PostgresQueryBuilder(this.table, this.dataSource);
|
|
610
|
-
param(tempBuilder);
|
|
611
|
-
this.addOperation("whereExists", { subquery: tempBuilder.operations });
|
|
612
|
-
}
|
|
613
|
-
else {
|
|
614
|
-
this.addOperation("whereNotNull", { field: param });
|
|
615
|
-
}
|
|
616
|
-
return this;
|
|
617
|
-
}
|
|
618
|
-
whereNotExists(param) {
|
|
619
|
-
if (typeof param === "function") {
|
|
620
|
-
const tempBuilder = new PostgresQueryBuilder(this.table, this.dataSource);
|
|
621
|
-
param(tempBuilder);
|
|
622
|
-
this.addOperation("whereNotExists", { subquery: tempBuilder.operations });
|
|
623
|
-
}
|
|
624
|
-
else {
|
|
625
|
-
this.addOperation("whereNull", { field: param });
|
|
626
|
-
}
|
|
627
|
-
return this;
|
|
628
|
-
}
|
|
629
|
-
whereSize(field, ...args) {
|
|
630
|
-
const operator = args.length === 2 ? args[0] : "=";
|
|
631
|
-
const size = args.length === 2 ? args[1] : args[0];
|
|
632
|
-
return this.whereArrayLength(field, operator, size);
|
|
633
|
-
}
|
|
634
|
-
/**
|
|
635
|
-
* Perform a full-text search.
|
|
636
|
-
*/
|
|
637
|
-
textSearch(query, filters) {
|
|
638
|
-
// Apply filters if provided
|
|
639
|
-
if (filters) {
|
|
640
|
-
for (const [key, value] of Object.entries(filters)) {
|
|
641
|
-
this.where(key, value);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
// Full-text search would need to know which columns to search
|
|
645
|
-
// For now, this is a placeholder - users should use whereFullText directly
|
|
646
|
-
return this;
|
|
647
|
-
}
|
|
648
|
-
/**
|
|
649
|
-
* Constrain an array field to contain the given value.
|
|
650
|
-
*/
|
|
651
|
-
whereArrayContains(field, value, key) {
|
|
652
|
-
if (key) {
|
|
653
|
-
// Array of objects - use JSON containment check
|
|
654
|
-
this.addOperation("whereRaw", {
|
|
655
|
-
expression: `${this.driver.dialect.quoteIdentifier(field)} @> ?::jsonb`,
|
|
656
|
-
bindings: [JSON.stringify([{ [key]: value }])],
|
|
657
|
-
});
|
|
658
|
-
}
|
|
659
|
-
else {
|
|
660
|
-
// Simple array - check if value is in array
|
|
661
|
-
this.addOperation("whereRaw", {
|
|
662
|
-
expression: `? = ANY(${this.driver.dialect.quoteIdentifier(field)})`,
|
|
663
|
-
bindings: [value],
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
return this;
|
|
667
|
-
}
|
|
668
|
-
/**
|
|
669
|
-
* Constrain an array field to not contain the given value.
|
|
670
|
-
*/
|
|
671
|
-
whereArrayNotContains(field, value, key) {
|
|
672
|
-
if (key) {
|
|
673
|
-
this.addOperation("whereRaw", {
|
|
674
|
-
expression: `NOT (${this.driver.dialect.quoteIdentifier(field)} @> ?::jsonb)`,
|
|
675
|
-
bindings: [JSON.stringify([{ [key]: value }])],
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
else {
|
|
679
|
-
this.addOperation("whereRaw", {
|
|
680
|
-
expression: `NOT (? = ANY(${this.driver.dialect.quoteIdentifier(field)}))`,
|
|
681
|
-
bindings: [value],
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
return this;
|
|
685
|
-
}
|
|
686
|
-
/**
|
|
687
|
-
* Constrain an array field to contain the value OR be empty.
|
|
688
|
-
*/
|
|
689
|
-
whereArrayHasOrEmpty(field, value, key) {
|
|
690
|
-
const quotedField = this.driver.dialect.quoteIdentifier(field);
|
|
691
|
-
if (key) {
|
|
692
|
-
this.addOperation("whereRaw", {
|
|
693
|
-
expression: `(${quotedField} @> ?::jsonb OR ${quotedField} = '[]'::jsonb OR ${quotedField} IS NULL)`,
|
|
694
|
-
bindings: [JSON.stringify([{ [key]: value }])],
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
else {
|
|
698
|
-
this.addOperation("whereRaw", {
|
|
699
|
-
expression: `(? = ANY(${quotedField}) OR array_length(${quotedField}, 1) IS NULL)`,
|
|
700
|
-
bindings: [value],
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
return this;
|
|
704
|
-
}
|
|
705
|
-
/**
|
|
706
|
-
* Constrain an array field to not contain the value OR be empty.
|
|
707
|
-
*/
|
|
708
|
-
whereArrayNotHaveOrEmpty(field, value, key) {
|
|
709
|
-
const quotedField = this.driver.dialect.quoteIdentifier(field);
|
|
710
|
-
if (key) {
|
|
711
|
-
this.addOperation("whereRaw", {
|
|
712
|
-
expression: `(NOT (${quotedField} @> ?::jsonb) OR ${quotedField} = '[]'::jsonb OR ${quotedField} IS NULL)`,
|
|
713
|
-
bindings: [JSON.stringify([{ [key]: value }])],
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
else {
|
|
717
|
-
this.addOperation("whereRaw", {
|
|
718
|
-
expression: `(NOT (? = ANY(${quotedField})) OR array_length(${quotedField}, 1) IS NULL)`,
|
|
719
|
-
bindings: [value],
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
return this;
|
|
723
|
-
}
|
|
724
|
-
join(...args) {
|
|
725
|
-
if (args.length === 3) {
|
|
726
|
-
this.addOperation("join", {
|
|
727
|
-
table: args[0],
|
|
728
|
-
localField: args[1],
|
|
729
|
-
foreignField: args[2],
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
else {
|
|
733
|
-
this.addOperation("join", args[0]);
|
|
734
|
-
}
|
|
735
|
-
return this;
|
|
736
|
-
}
|
|
737
|
-
leftJoin(...args) {
|
|
738
|
-
if (args.length === 3) {
|
|
739
|
-
this.addOperation("leftJoin", {
|
|
740
|
-
table: args[0],
|
|
741
|
-
localField: args[1],
|
|
742
|
-
foreignField: args[2],
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
else {
|
|
746
|
-
this.addOperation("leftJoin", args[0]);
|
|
747
|
-
}
|
|
748
|
-
return this;
|
|
749
|
-
}
|
|
750
|
-
rightJoin(...args) {
|
|
751
|
-
if (args.length === 3) {
|
|
752
|
-
this.addOperation("rightJoin", {
|
|
753
|
-
table: args[0],
|
|
754
|
-
localField: args[1],
|
|
755
|
-
foreignField: args[2],
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
else {
|
|
759
|
-
this.addOperation("rightJoin", args[0]);
|
|
760
|
-
}
|
|
761
|
-
return this;
|
|
762
|
-
}
|
|
763
|
-
innerJoin(...args) {
|
|
764
|
-
if (args.length === 3) {
|
|
765
|
-
this.addOperation("innerJoin", {
|
|
766
|
-
table: args[0],
|
|
767
|
-
localField: args[1],
|
|
768
|
-
foreignField: args[2],
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
else {
|
|
772
|
-
this.addOperation("innerJoin", args[0]);
|
|
773
|
-
}
|
|
774
|
-
return this;
|
|
775
|
-
}
|
|
776
|
-
fullJoin(...args) {
|
|
777
|
-
if (args.length === 3) {
|
|
778
|
-
this.addOperation("fullJoin", {
|
|
779
|
-
table: args[0],
|
|
780
|
-
localField: args[1],
|
|
781
|
-
foreignField: args[2],
|
|
782
|
-
});
|
|
783
|
-
}
|
|
784
|
-
else {
|
|
785
|
-
this.addOperation("fullJoin", args[0]);
|
|
786
|
-
}
|
|
787
|
-
return this;
|
|
788
|
-
}
|
|
789
|
-
/**
|
|
790
|
-
* Add a CROSS JOIN clause.
|
|
791
|
-
*/
|
|
792
|
-
crossJoin(table) {
|
|
793
|
-
this.addOperation("crossJoin", { table });
|
|
794
|
-
return this;
|
|
795
|
-
}
|
|
796
|
-
/**
|
|
797
|
-
* Add a raw JOIN clause.
|
|
798
|
-
*/
|
|
799
|
-
joinRaw(expression, bindings) {
|
|
800
|
-
this.addOperation("joinRaw", { expression, bindings });
|
|
801
|
-
return this;
|
|
802
|
-
}
|
|
803
|
-
select(...args) {
|
|
804
|
-
// Handle single array argument
|
|
805
|
-
if (args.length === 1 && Array.isArray(args[0])) {
|
|
806
|
-
this.addOperation("select", { fields: args[0] });
|
|
807
|
-
}
|
|
808
|
-
// Handle Record<string, 0 | 1 | boolean> (projection map)
|
|
809
|
-
else if (args.length === 1 && typeof args[0] === "object" && !Array.isArray(args[0])) {
|
|
810
|
-
this.addOperation("select", { fields: args[0] });
|
|
811
|
-
}
|
|
812
|
-
// Handle rest params (...fields)
|
|
813
|
-
else {
|
|
814
|
-
const flatFields = args.flat();
|
|
815
|
-
this.addOperation("select", { fields: flatFields });
|
|
816
|
-
}
|
|
817
|
-
return this;
|
|
818
|
-
}
|
|
819
|
-
/**
|
|
820
|
-
* Select a field with alias.
|
|
821
|
-
*/
|
|
822
|
-
selectAs(field, alias) {
|
|
823
|
-
// Use select operation with alias format (Record<field, alias>)
|
|
824
|
-
this.addOperation("select", { fields: { [field]: alias } });
|
|
825
|
-
return this;
|
|
826
|
-
}
|
|
827
|
-
/**
|
|
828
|
-
* Select raw expression.
|
|
829
|
-
*/
|
|
830
|
-
selectRaw(expression, bindings) {
|
|
831
|
-
this.addOperation("selectRaw", { expression, bindings });
|
|
832
|
-
return this;
|
|
833
|
-
}
|
|
834
|
-
/**
|
|
835
|
-
* Select multiple raw expressions.
|
|
836
|
-
*/
|
|
837
|
-
selectRawMany(definitions) {
|
|
838
|
-
for (const def of definitions) {
|
|
839
|
-
this.selectRaw({ [def.alias]: def.expression }, def.bindings);
|
|
840
|
-
}
|
|
841
|
-
return this;
|
|
842
|
-
}
|
|
843
|
-
/**
|
|
844
|
-
* Select subquery.
|
|
845
|
-
*/
|
|
846
|
-
selectSub(expression, alias) {
|
|
847
|
-
this.addOperation("selectRaw", { expression: { [alias]: expression } });
|
|
848
|
-
return this;
|
|
849
|
-
}
|
|
850
|
-
/**
|
|
851
|
-
* Add subquery to existing selection.
|
|
852
|
-
*/
|
|
853
|
-
addSelectSub(expression, alias) {
|
|
854
|
-
return this.selectSub(expression, alias);
|
|
855
|
-
}
|
|
856
|
-
/**
|
|
857
|
-
* Select aggregate value.
|
|
858
|
-
*/
|
|
859
|
-
selectAggregate(field, aggregate, alias) {
|
|
860
|
-
const expr = `${aggregate.toUpperCase()}(${field})`;
|
|
861
|
-
return this.selectRaw({ [alias]: expr });
|
|
862
|
-
}
|
|
863
|
-
/**
|
|
864
|
-
* Select existence check.
|
|
865
|
-
*/
|
|
866
|
-
selectExists(field, alias) {
|
|
867
|
-
return this.selectRaw({ [alias]: `${field} IS NOT NULL` });
|
|
868
|
-
}
|
|
869
|
-
/**
|
|
870
|
-
* Select count.
|
|
871
|
-
*/
|
|
872
|
-
selectCount(field, alias) {
|
|
873
|
-
return this.selectAggregate(field, "count", alias);
|
|
874
|
-
}
|
|
875
|
-
/**
|
|
876
|
-
* Select CASE expression.
|
|
877
|
-
*/
|
|
878
|
-
selectCase(cases, otherwise, alias) {
|
|
879
|
-
const caseExpr = cases.map((c) => `WHEN ${c.when} THEN ${c.then}`).join(" ");
|
|
880
|
-
return this.selectRaw({ [alias]: `CASE ${caseExpr} ELSE ${otherwise} END` });
|
|
881
|
-
}
|
|
882
|
-
/**
|
|
883
|
-
* Select conditional (IF/ELSE).
|
|
884
|
-
*/
|
|
885
|
-
selectWhen(condition, thenValue, elseValue, alias) {
|
|
886
|
-
return this.selectRaw({
|
|
887
|
-
[alias]: `CASE WHEN ${condition} THEN ${thenValue} ELSE ${elseValue} END`,
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
/**
|
|
891
|
-
* Direct projection manipulation.
|
|
892
|
-
*/
|
|
893
|
-
selectDriverProjection(callback) {
|
|
894
|
-
// PostgreSQL doesn't have direct projection manipulation like MongoDB
|
|
895
|
-
return this;
|
|
896
|
-
}
|
|
897
|
-
/**
|
|
898
|
-
* Select JSON path.
|
|
899
|
-
*/
|
|
900
|
-
selectJson(path, alias) {
|
|
901
|
-
const parts = path.split("->");
|
|
902
|
-
const column = parts[0];
|
|
903
|
-
const jsonPath = parts.slice(1).join("->");
|
|
904
|
-
const expr = jsonPath ? `${column}->>'${jsonPath}'` : column;
|
|
905
|
-
return alias ? this.selectAs(expr, alias) : this.selectRaw(expr);
|
|
906
|
-
}
|
|
907
|
-
/**
|
|
908
|
-
* Select JSON path with raw expression.
|
|
909
|
-
*/
|
|
910
|
-
selectJsonRaw(path, expression, alias) {
|
|
911
|
-
return this.selectRaw({ [alias]: expression });
|
|
912
|
-
}
|
|
913
|
-
/**
|
|
914
|
-
* Exclude JSON path.
|
|
915
|
-
*/
|
|
916
|
-
deselectJson(path) {
|
|
917
|
-
return this.deselect([path]);
|
|
918
|
-
}
|
|
919
|
-
/**
|
|
920
|
-
* Concatenate fields.
|
|
921
|
-
*/
|
|
922
|
-
selectConcat(fields, alias) {
|
|
923
|
-
const expr = fields.join(" || ");
|
|
924
|
-
return this.selectRaw({ [alias]: expr });
|
|
925
|
-
}
|
|
926
|
-
/**
|
|
927
|
-
* Coalesce values.
|
|
928
|
-
*/
|
|
929
|
-
selectCoalesce(fields, alias) {
|
|
930
|
-
const expr = `COALESCE(${fields.join(", ")})`;
|
|
931
|
-
return this.selectRaw({ [alias]: expr });
|
|
932
|
-
}
|
|
933
|
-
/**
|
|
934
|
-
* Window function.
|
|
935
|
-
*/
|
|
936
|
-
selectWindow(spec) {
|
|
937
|
-
this.addOperation("selectRaw", { expression: spec });
|
|
938
|
-
return this;
|
|
939
|
-
}
|
|
940
|
-
/**
|
|
941
|
-
* Exclude columns from projection.
|
|
942
|
-
*/
|
|
943
|
-
deselect(fields) {
|
|
944
|
-
this.addOperation("deselect", { fields });
|
|
945
|
-
return this;
|
|
946
|
-
}
|
|
947
|
-
/**
|
|
948
|
-
* Clear selection.
|
|
949
|
-
*/
|
|
950
|
-
clearSelect() {
|
|
951
|
-
this.operations = this.operations.filter((op) => !op.type.startsWith("select") && op.type !== "deselect");
|
|
952
|
-
return this;
|
|
953
|
-
}
|
|
954
|
-
/**
|
|
955
|
-
* Select all columns.
|
|
956
|
-
*/
|
|
957
|
-
selectAll() {
|
|
958
|
-
return this.clearSelect();
|
|
959
|
-
}
|
|
960
|
-
/**
|
|
961
|
-
* Restore default projection.
|
|
962
|
-
*/
|
|
963
|
-
selectDefault() {
|
|
964
|
-
return this.clearSelect();
|
|
965
|
-
}
|
|
966
|
-
/**
|
|
967
|
-
* Select distinct values.
|
|
968
|
-
*/
|
|
969
|
-
distinctValues(fields) {
|
|
970
|
-
this.addOperation("distinct", {});
|
|
971
|
-
if (fields) {
|
|
972
|
-
this.select(Array.isArray(fields) ? fields : [fields]);
|
|
973
|
-
}
|
|
974
|
-
return this;
|
|
975
|
-
}
|
|
976
|
-
/**
|
|
977
|
-
* Add additional select fields.
|
|
978
|
-
*/
|
|
979
|
-
addSelect(fields) {
|
|
980
|
-
this.addOperation("select", { fields, add: true });
|
|
981
|
-
return this;
|
|
982
|
-
}
|
|
983
|
-
orderBy(...args) {
|
|
984
|
-
if (typeof args[0] === "string") {
|
|
985
|
-
this.addOperation("orderBy", { field: args[0], direction: args[1] ?? "asc" });
|
|
986
|
-
}
|
|
987
|
-
else {
|
|
988
|
-
for (const [field, direction] of Object.entries(args[0])) {
|
|
989
|
-
this.addOperation("orderBy", { field, direction });
|
|
296
|
+
* Load relations via SQL JOINs (single query) with optional per-relation constraints.
|
|
297
|
+
*
|
|
298
|
+
* Supports:
|
|
299
|
+
* - `joinWith("author")` / `joinWith(["author", "category"])`
|
|
300
|
+
* - `joinWith({ actions: q => q.where("status", "pending").limit(5) })`
|
|
301
|
+
* - `joinWith({ organizationAiModel: "id,name", actions: q => q.orderBy("sort_order") })`
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ChatMessage.joinWith({
|
|
305
|
+
* actions: q => q.where("status", "pending").orderBy("sort_order", "asc").limit(5),
|
|
306
|
+
* organizationAiModel: "id,createdAt",
|
|
307
|
+
* })
|
|
308
|
+
*/
|
|
309
|
+
joinWith(...args) {
|
|
310
|
+
const entries = [];
|
|
311
|
+
for (const arg of args) {
|
|
312
|
+
if (typeof arg === "string") {
|
|
313
|
+
entries.push({ path: arg });
|
|
990
314
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
/**
|
|
995
|
-
* Order descending.
|
|
996
|
-
*/
|
|
997
|
-
orderByDesc(field) {
|
|
998
|
-
return this.orderBy(field, "desc");
|
|
999
|
-
}
|
|
1000
|
-
/**
|
|
1001
|
-
* Order with raw expression.
|
|
1002
|
-
*/
|
|
1003
|
-
orderByRaw(expression, bindings) {
|
|
1004
|
-
this.addOperation("orderByRaw", { expression, bindings });
|
|
1005
|
-
return this;
|
|
1006
|
-
}
|
|
1007
|
-
/**
|
|
1008
|
-
* Order randomly.
|
|
1009
|
-
*/
|
|
1010
|
-
orderByRandom(limit) {
|
|
1011
|
-
this.addOperation("orderByRaw", { expression: "RANDOM()" });
|
|
1012
|
-
return this.limit(limit);
|
|
1013
|
-
}
|
|
1014
|
-
/**
|
|
1015
|
-
* Get latest records.
|
|
1016
|
-
*/
|
|
1017
|
-
async latest(column = "createdAt") {
|
|
1018
|
-
return this.orderBy(column, "desc").get();
|
|
1019
|
-
}
|
|
1020
|
-
/**
|
|
1021
|
-
* Get oldest records.
|
|
1022
|
-
*/
|
|
1023
|
-
oldest(column = "createdAt") {
|
|
1024
|
-
return this.orderBy(column, "asc");
|
|
1025
|
-
}
|
|
1026
|
-
// ============================================================================
|
|
1027
|
-
// LIMITING / PAGINATION
|
|
1028
|
-
// ============================================================================
|
|
1029
|
-
/**
|
|
1030
|
-
* Limit results.
|
|
1031
|
-
*/
|
|
1032
|
-
limit(value) {
|
|
1033
|
-
this.addOperation("limit", { value });
|
|
1034
|
-
return this;
|
|
1035
|
-
}
|
|
1036
|
-
/**
|
|
1037
|
-
* Skip results.
|
|
1038
|
-
*/
|
|
1039
|
-
skip(value) {
|
|
1040
|
-
this.addOperation("offset", { value });
|
|
1041
|
-
return this;
|
|
1042
|
-
}
|
|
1043
|
-
/**
|
|
1044
|
-
* Offset results.
|
|
1045
|
-
*/
|
|
1046
|
-
offset(value) {
|
|
1047
|
-
return this.skip(value);
|
|
1048
|
-
}
|
|
1049
|
-
/**
|
|
1050
|
-
* Take first N results.
|
|
1051
|
-
*/
|
|
1052
|
-
take(value) {
|
|
1053
|
-
return this.limit(value);
|
|
1054
|
-
}
|
|
1055
|
-
/**
|
|
1056
|
-
* Apply cursor pagination hints.
|
|
1057
|
-
*/
|
|
1058
|
-
cursor(after, before) {
|
|
1059
|
-
// Store cursor hints for cursorPaginate
|
|
1060
|
-
return this;
|
|
1061
|
-
}
|
|
1062
|
-
// ============================================================================
|
|
1063
|
-
// GROUPING / AGGREGATION
|
|
1064
|
-
// ============================================================================
|
|
1065
|
-
/**
|
|
1066
|
-
* Group results.
|
|
1067
|
-
*/
|
|
1068
|
-
groupBy(input) {
|
|
1069
|
-
const fields = Array.isArray(input) ? input : [input];
|
|
1070
|
-
this.addOperation("groupBy", { fields });
|
|
1071
|
-
return this;
|
|
1072
|
-
}
|
|
1073
|
-
/**
|
|
1074
|
-
* Raw GROUP BY.
|
|
1075
|
-
*/
|
|
1076
|
-
groupByRaw(expression, bindings) {
|
|
1077
|
-
this.addOperation("groupBy", { expression, bindings });
|
|
1078
|
-
return this;
|
|
1079
|
-
}
|
|
1080
|
-
having(...args) {
|
|
1081
|
-
if (args.length === 1) {
|
|
1082
|
-
const input = args[0];
|
|
1083
|
-
if (Array.isArray(input)) {
|
|
1084
|
-
if (input.length === 2) {
|
|
1085
|
-
this.addOperation("having", { field: input[0], operator: "=", value: input[1] });
|
|
1086
|
-
}
|
|
1087
|
-
else {
|
|
1088
|
-
this.addOperation("having", { field: input[0], operator: input[1], value: input[2] });
|
|
315
|
+
else if (Array.isArray(arg)) {
|
|
316
|
+
for (const rel of arg) {
|
|
317
|
+
entries.push({ path: rel });
|
|
1089
318
|
}
|
|
1090
319
|
}
|
|
1091
|
-
else {
|
|
1092
|
-
for (const [
|
|
1093
|
-
|
|
320
|
+
else if (typeof arg === "object" && arg !== null) {
|
|
321
|
+
for (const [rel, val] of Object.entries(arg)) {
|
|
322
|
+
entries.push({ path: rel, constraint: val });
|
|
1094
323
|
}
|
|
1095
324
|
}
|
|
1096
325
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
if (this.fetchingCallback) {
|
|
1123
|
-
await this.fetchingCallback(this);
|
|
1124
|
-
}
|
|
1125
|
-
const parser = new postgresQueryParser.PostgresQueryParser({
|
|
1126
|
-
table: this.table,
|
|
1127
|
-
operations: this.operations,
|
|
1128
|
-
});
|
|
1129
|
-
const { sql, params } = parser.parse();
|
|
1130
|
-
try {
|
|
1131
|
-
const result = await this.driver.query(sql, params);
|
|
1132
|
-
let records = result.rows;
|
|
1133
|
-
// Extract joined relation data before hydration
|
|
1134
|
-
const joinedData = this.extractJoinedRelationData(records);
|
|
1135
|
-
if (this.hydratingCallback) {
|
|
1136
|
-
await this.hydratingCallback(records, {});
|
|
1137
|
-
}
|
|
1138
|
-
if (this.hydrateCallback) {
|
|
1139
|
-
records = records.map((row, index) => this.hydrateCallback(row, index));
|
|
1140
|
-
}
|
|
1141
|
-
// Attach joined relations to hydrated models
|
|
1142
|
-
this.attachJoinedRelations(records, joinedData);
|
|
1143
|
-
if (this.fetchedCallback) {
|
|
1144
|
-
await this.fetchedCallback(records, {});
|
|
1145
|
-
}
|
|
1146
|
-
// Cleanup
|
|
1147
|
-
this.operations = [];
|
|
1148
|
-
return records;
|
|
1149
|
-
}
|
|
1150
|
-
catch (error) {
|
|
1151
|
-
console.log("Error while executing:", sql, params);
|
|
1152
|
-
console.log("Query Builder Error:", error);
|
|
1153
|
-
throw error;
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
/**
|
|
1157
|
-
* Apply JOIN operations for joinWith() relations.
|
|
1158
|
-
*/
|
|
1159
|
-
applyJoinRelations() {
|
|
1160
|
-
if (this.joinRelations.size === 0 || !this.relationDefinitions)
|
|
1161
|
-
return;
|
|
1162
|
-
for (const [relationName, config] of this.joinRelations) {
|
|
1163
|
-
const relationDef = this.relationDefinitions[relationName];
|
|
1164
|
-
if (!relationDef)
|
|
1165
|
-
continue;
|
|
1166
|
-
// Resolve the related model class
|
|
1167
|
-
const RelatedModel = typeof relationDef.model === "string"
|
|
1168
|
-
? registerModel.getModelFromRegistry(relationDef.model)
|
|
1169
|
-
: relationDef.model;
|
|
1170
|
-
if (!RelatedModel)
|
|
1171
|
-
continue;
|
|
1172
|
-
const relatedTable = RelatedModel.table;
|
|
1173
|
-
const alias = config.alias;
|
|
1174
|
-
// Determine join keys based on relation type
|
|
1175
|
-
let localField;
|
|
1176
|
-
let foreignField;
|
|
1177
|
-
if (relationDef.type === "belongsTo") {
|
|
1178
|
-
localField = relationDef.foreignKey || `${relationName}Id`;
|
|
1179
|
-
foreignField = relationDef.ownerKey || "id";
|
|
1180
|
-
}
|
|
1181
|
-
else {
|
|
1182
|
-
// hasOne, hasMany
|
|
1183
|
-
localField = relationDef.localKey || "id";
|
|
1184
|
-
foreignField = relationDef.foreignKey || `${this.table.slice(0, -1)}Id`;
|
|
1185
|
-
}
|
|
1186
|
-
// Add LEFT JOIN operation
|
|
1187
|
-
this.addOperation("leftJoin", {
|
|
1188
|
-
table: relatedTable,
|
|
1189
|
-
alias,
|
|
1190
|
-
localField,
|
|
1191
|
-
foreignField,
|
|
1192
|
-
});
|
|
1193
|
-
// Add SELECT for related columns with prefix
|
|
1194
|
-
this.addOperation("selectRelatedColumns", {
|
|
1195
|
-
alias,
|
|
1196
|
-
relationName,
|
|
1197
|
-
table: relatedTable,
|
|
1198
|
-
});
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
/**
|
|
1202
|
-
* Extract joined relation data from raw records.
|
|
1203
|
-
* Returns a map of record index to relation data.
|
|
1204
|
-
*/
|
|
1205
|
-
extractJoinedRelationData(records) {
|
|
1206
|
-
const result = new Map();
|
|
1207
|
-
if (this.joinRelations.size === 0)
|
|
1208
|
-
return result;
|
|
1209
|
-
records.forEach((record, index) => {
|
|
1210
|
-
const relationData = {};
|
|
1211
|
-
for (const [relationName, config] of this.joinRelations) {
|
|
1212
|
-
const columnName = config.alias; // e.g., "_rel_author"
|
|
1213
|
-
// Get the JSON object from the row_to_json column
|
|
1214
|
-
const relatedData = record[columnName];
|
|
1215
|
-
// Remove from main record so it doesn't appear in model.data
|
|
1216
|
-
delete record[columnName];
|
|
1217
|
-
// If null or all values are null, set to null
|
|
1218
|
-
if (relatedData === null ||
|
|
1219
|
-
(typeof relatedData === "object" && Object.values(relatedData).every((v) => v === null))) {
|
|
1220
|
-
relationData[relationName] = null;
|
|
1221
|
-
}
|
|
1222
|
-
else {
|
|
1223
|
-
relationData[relationName] = relatedData;
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
result.set(index, relationData);
|
|
1227
|
-
});
|
|
1228
|
-
return result;
|
|
1229
|
-
}
|
|
1230
|
-
/**
|
|
1231
|
-
* Attach joined relations to hydrated models.
|
|
1232
|
-
*/
|
|
1233
|
-
attachJoinedRelations(records, joinedData) {
|
|
1234
|
-
if (this.joinRelations.size === 0 || !this.relationDefinitions)
|
|
1235
|
-
return;
|
|
1236
|
-
records.forEach((model, index) => {
|
|
1237
|
-
const relationData = joinedData.get(index);
|
|
1238
|
-
if (!relationData)
|
|
1239
|
-
return;
|
|
1240
|
-
for (const [relationName, data] of Object.entries(relationData)) {
|
|
1241
|
-
if (data === null) {
|
|
1242
|
-
// No related record
|
|
1243
|
-
model[relationName] = null;
|
|
1244
|
-
if (model.loadedRelations) {
|
|
1245
|
-
model.loadedRelations.set(relationName, null);
|
|
326
|
+
for (const { path, constraint } of entries) {
|
|
327
|
+
// Parse each dot-notation path segment (supports "rel1.rel2" nesting)
|
|
328
|
+
const segments = path.split(".");
|
|
329
|
+
let currentModel = this.modelClass;
|
|
330
|
+
let currentPath = "";
|
|
331
|
+
for (let i = 0; i < segments.length; i++) {
|
|
332
|
+
const rawSeg = segments[i];
|
|
333
|
+
// String shorthand: "relName:col1,col2"
|
|
334
|
+
const colonIdx = rawSeg.indexOf(":");
|
|
335
|
+
const segName = colonIdx === -1 ? rawSeg : rawSeg.slice(0, colonIdx);
|
|
336
|
+
const segColumns = colonIdx === -1
|
|
337
|
+
? undefined
|
|
338
|
+
: rawSeg
|
|
339
|
+
.slice(colonIdx + 1)
|
|
340
|
+
.split(",")
|
|
341
|
+
.filter(Boolean);
|
|
342
|
+
currentPath = currentPath ? `${currentPath}.${segName}` : segName;
|
|
343
|
+
// If already registered, update if new select columns given; advance model
|
|
344
|
+
if (this.joinRelations.has(currentPath)) {
|
|
345
|
+
const existing = this.joinRelations.get(currentPath);
|
|
346
|
+
if (segColumns)
|
|
347
|
+
existing.select = segColumns;
|
|
348
|
+
// Apply constraint only on the deepest segment
|
|
349
|
+
if (i === segments.length - 1 && constraint !== undefined) {
|
|
350
|
+
existing.constraintOps = this._resolveConstraintOps(constraint);
|
|
1246
351
|
}
|
|
352
|
+
currentModel =
|
|
353
|
+
typeof existing.model === "string"
|
|
354
|
+
? registerModel.getModelFromRegistry(existing.model)
|
|
355
|
+
: existing.model;
|
|
1247
356
|
continue;
|
|
1248
357
|
}
|
|
1249
|
-
|
|
1250
|
-
if (!relationDef)
|
|
358
|
+
if (!this.relationDefinitions)
|
|
1251
359
|
continue;
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
360
|
+
const def = (i === 0
|
|
361
|
+
? this.relationDefinitions
|
|
362
|
+
: currentModel?.relations)?.[segName];
|
|
363
|
+
if (!def) {
|
|
364
|
+
throw new Error(`Relation "${segName}" not found on model ${currentModel?.name ?? "unknown"}`);
|
|
365
|
+
}
|
|
366
|
+
// Resolve select columns: colon shorthand > constraint string > def.select
|
|
367
|
+
let selectColumns = segColumns ?? def.select;
|
|
368
|
+
let constraintOps;
|
|
369
|
+
if (i === segments.length - 1 && constraint !== undefined) {
|
|
370
|
+
if (typeof constraint === "string") {
|
|
371
|
+
selectColumns = constraint.split(",").filter(Boolean);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
constraintOps = this._resolveConstraintOps(constraint);
|
|
1262
375
|
}
|
|
1263
376
|
}
|
|
377
|
+
const alias = currentPath.replace(/\./g, "_");
|
|
378
|
+
this.joinRelations.set(currentPath, {
|
|
379
|
+
alias,
|
|
380
|
+
type: def.type,
|
|
381
|
+
model: def.model,
|
|
382
|
+
localKey: def.localKey,
|
|
383
|
+
foreignKey: def.foreignKey,
|
|
384
|
+
ownerKey: def.ownerKey,
|
|
385
|
+
parentPath: i > 0 ? currentPath.substring(0, currentPath.lastIndexOf(".")) : null,
|
|
386
|
+
relationName: segName,
|
|
387
|
+
parentModel: currentModel,
|
|
388
|
+
select: selectColumns,
|
|
389
|
+
constraintOps,
|
|
390
|
+
});
|
|
391
|
+
currentModel =
|
|
392
|
+
typeof def.model === "string" ? registerModel.getModelFromRegistry(def.model) : def.model;
|
|
393
|
+
if (!currentModel) {
|
|
394
|
+
throw new Error(`Relation model not found for "${segName}" in "${currentPath}"`);
|
|
395
|
+
}
|
|
1264
396
|
}
|
|
1265
|
-
}
|
|
397
|
+
}
|
|
398
|
+
return this;
|
|
1266
399
|
}
|
|
400
|
+
/** Run a joinWith constraint callback against a sub-QB and capture its operations. */
|
|
401
|
+
_resolveConstraintOps(constraint) {
|
|
402
|
+
if (typeof constraint === "string")
|
|
403
|
+
return [];
|
|
404
|
+
const sub = new PostgresQueryBuilder("__sub__", this.dataSource);
|
|
405
|
+
constraint(sub);
|
|
406
|
+
return sub.operations;
|
|
407
|
+
}
|
|
408
|
+
// ============================================================================
|
|
409
|
+
// EXECUTION METHODS
|
|
410
|
+
// ============================================================================
|
|
1267
411
|
/**
|
|
1268
|
-
*
|
|
412
|
+
* Execute the query and return all matching rows.
|
|
1269
413
|
*/
|
|
414
|
+
async get() {
|
|
415
|
+
this.applyPendingScopes();
|
|
416
|
+
this._processJoinWithOps();
|
|
417
|
+
this.applyJoinRelations();
|
|
418
|
+
if (this.fetchingCallback) {
|
|
419
|
+
await this.fetchingCallback(this);
|
|
420
|
+
}
|
|
421
|
+
const parser = new postgresQueryParser.PostgresQueryParser({
|
|
422
|
+
table: this.table,
|
|
423
|
+
operations: toParserOps(this.operations),
|
|
424
|
+
});
|
|
425
|
+
const { query = "", bindings = [] } = parser.parse();
|
|
426
|
+
try {
|
|
427
|
+
const result = await this.driver.query(query, bindings);
|
|
428
|
+
let records = result.rows;
|
|
429
|
+
const joinedData = this.extractJoinedRelationData(records);
|
|
430
|
+
if (this.hydratingCallback) {
|
|
431
|
+
await this.hydratingCallback(records, {});
|
|
432
|
+
}
|
|
433
|
+
if (this.hydrateCallback) {
|
|
434
|
+
records = records.map((row, index) => this.hydrateCallback(row, index));
|
|
435
|
+
}
|
|
436
|
+
this.attachJoinedRelations(records, joinedData);
|
|
437
|
+
if (this.fetchedCallback) {
|
|
438
|
+
await this.fetchedCallback(records, {});
|
|
439
|
+
}
|
|
440
|
+
this.operations = [];
|
|
441
|
+
return records;
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
console.log("Error while executing:", query, bindings);
|
|
445
|
+
console.log("Query Builder Error:", error);
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/** Get first result. */
|
|
1270
450
|
async first() {
|
|
1271
451
|
const results = await this.limit(1).get();
|
|
1272
452
|
return results[0] ?? null;
|
|
1273
453
|
}
|
|
1274
|
-
/**
|
|
1275
|
-
* Get last result.
|
|
1276
|
-
*/
|
|
454
|
+
/** Get last result (by id desc). */
|
|
1277
455
|
async last() {
|
|
1278
456
|
const results = await this.orderByDesc("id").limit(1).get();
|
|
1279
457
|
return results[0] ?? null;
|
|
1280
458
|
}
|
|
1281
|
-
/**
|
|
1282
|
-
* Get random results.
|
|
1283
|
-
*/
|
|
459
|
+
/** Get random results. */
|
|
1284
460
|
async random(limit) {
|
|
1285
461
|
this.orderByRaw("RANDOM()");
|
|
1286
|
-
if (limit)
|
|
462
|
+
if (limit)
|
|
1287
463
|
this.limit(limit);
|
|
1288
|
-
}
|
|
1289
464
|
return this.get();
|
|
1290
465
|
}
|
|
1291
|
-
/**
|
|
1292
|
-
* Get first or throw.
|
|
1293
|
-
*/
|
|
466
|
+
/** Get first or throw. */
|
|
1294
467
|
async firstOrFail() {
|
|
1295
468
|
const result = await this.first();
|
|
1296
|
-
if (!result)
|
|
469
|
+
if (!result)
|
|
1297
470
|
throw new Error("No records found");
|
|
1298
|
-
}
|
|
1299
471
|
return result;
|
|
1300
472
|
}
|
|
1301
|
-
/**
|
|
1302
|
-
* Get first or call callback.
|
|
1303
|
-
*/
|
|
473
|
+
/** Get first or call callback. */
|
|
1304
474
|
async firstOr(callback) {
|
|
1305
475
|
const result = await this.first();
|
|
1306
476
|
return result ?? (await callback());
|
|
1307
477
|
}
|
|
1308
|
-
/**
|
|
1309
|
-
* Get first or return default.
|
|
1310
|
-
*/
|
|
478
|
+
/** Get first or return null. */
|
|
1311
479
|
async firstOrNull() {
|
|
1312
480
|
return this.first();
|
|
1313
481
|
}
|
|
1314
|
-
/**
|
|
1315
|
-
* Get first or create new.
|
|
1316
|
-
*/
|
|
482
|
+
/** Get first or return default. */
|
|
1317
483
|
async firstOrNew(defaults) {
|
|
1318
484
|
const result = await this.first();
|
|
1319
485
|
return result ?? defaults;
|
|
1320
486
|
}
|
|
1321
|
-
/**
|
|
1322
|
-
* Find by ID.
|
|
1323
|
-
*/
|
|
487
|
+
/** Find by primary key. */
|
|
1324
488
|
async find(id) {
|
|
1325
489
|
return this.where("id", id).first();
|
|
1326
490
|
}
|
|
1327
|
-
/**
|
|
1328
|
-
* Count results.
|
|
1329
|
-
*/
|
|
491
|
+
/** Count matching rows. */
|
|
1330
492
|
async count() {
|
|
1331
493
|
this.applyPendingScopes();
|
|
1332
|
-
|
|
1333
|
-
const countOps = [
|
|
494
|
+
const countOps = toParserOps([
|
|
1334
495
|
...this.operations.filter((op) => op.type.includes("where") || op.type.includes("join")),
|
|
1335
496
|
{ type: "selectRaw", data: { expression: 'COUNT(*) AS "count"' } },
|
|
1336
|
-
];
|
|
1337
|
-
const parser = new postgresQueryParser.PostgresQueryParser({
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
});
|
|
1341
|
-
const { sql, params } = parser.parse();
|
|
1342
|
-
const result = await this.driver.query(sql, params);
|
|
497
|
+
]);
|
|
498
|
+
const parser = new postgresQueryParser.PostgresQueryParser({ table: this.table, operations: countOps });
|
|
499
|
+
const { query = "", bindings = [] } = parser.parse();
|
|
500
|
+
const result = await this.driver.query(query, bindings);
|
|
1343
501
|
return parseInt(result.rows[0]?.count ?? "0", 10);
|
|
1344
502
|
}
|
|
1345
|
-
/**
|
|
1346
|
-
* Sum of field values.
|
|
1347
|
-
*/
|
|
503
|
+
/** SUM a numeric field. */
|
|
1348
504
|
async sum(field) {
|
|
1349
505
|
this.applyPendingScopes();
|
|
1350
506
|
const result = await this.selectRaw(`SUM(${field}) as sum`).first();
|
|
1351
507
|
return parseFloat(result?.sum ?? "0");
|
|
1352
508
|
}
|
|
1353
|
-
/**
|
|
1354
|
-
* Average of field values.
|
|
1355
|
-
*/
|
|
509
|
+
/** AVG of a numeric field. */
|
|
1356
510
|
async avg(field) {
|
|
1357
511
|
this.applyPendingScopes();
|
|
1358
512
|
const result = await this.selectRaw(`AVG(${field}) as avg`).first();
|
|
1359
513
|
return parseFloat(result?.avg ?? "0");
|
|
1360
514
|
}
|
|
1361
|
-
/**
|
|
1362
|
-
* Minimum field value.
|
|
1363
|
-
*/
|
|
515
|
+
/** MIN of a numeric field. */
|
|
1364
516
|
async min(field) {
|
|
1365
517
|
this.applyPendingScopes();
|
|
1366
518
|
const result = await this.selectRaw(`MIN(${field}) as min`).first();
|
|
1367
519
|
return parseFloat(result?.min ?? "0");
|
|
1368
520
|
}
|
|
1369
|
-
/**
|
|
1370
|
-
* Maximum field value.
|
|
1371
|
-
*/
|
|
521
|
+
/** MAX of a numeric field. */
|
|
1372
522
|
async max(field) {
|
|
1373
523
|
this.applyPendingScopes();
|
|
1374
524
|
const result = await this.selectRaw(`MAX(${field}) as max`).first();
|
|
1375
525
|
return parseFloat(result?.max ?? "0");
|
|
1376
526
|
}
|
|
1377
|
-
/**
|
|
1378
|
-
* Get distinct values.
|
|
1379
|
-
*/
|
|
527
|
+
/** Get distinct values for a field. */
|
|
1380
528
|
async distinct(field) {
|
|
1381
529
|
this.distinctValues(field);
|
|
1382
530
|
const results = await this.get();
|
|
1383
531
|
return results.map((row) => row[field]);
|
|
1384
532
|
}
|
|
1385
|
-
/**
|
|
1386
|
-
* Get array of values for field.
|
|
1387
|
-
*/
|
|
533
|
+
/** Get array of all values for a single field. */
|
|
1388
534
|
async pluck(field) {
|
|
1389
535
|
const results = await this.select([field]).get();
|
|
1390
536
|
return results.map((row) => row[field]);
|
|
1391
537
|
}
|
|
1392
|
-
/**
|
|
1393
|
-
* Get single scalar value.
|
|
1394
|
-
*/
|
|
538
|
+
/** Get a single scalar value. */
|
|
1395
539
|
async value(field) {
|
|
1396
540
|
const result = await this.select([field]).first();
|
|
1397
541
|
return result?.[field] ?? null;
|
|
1398
542
|
}
|
|
1399
|
-
/**
|
|
1400
|
-
* Check if records exist.
|
|
1401
|
-
*/
|
|
543
|
+
/** Check whether any matching rows exist. */
|
|
1402
544
|
async exists() {
|
|
1403
545
|
const count = await this.limit(1).count();
|
|
1404
546
|
return count > 0;
|
|
1405
547
|
}
|
|
1406
|
-
/**
|
|
1407
|
-
* Check if no records exist.
|
|
1408
|
-
*/
|
|
548
|
+
/** Check whether NO matching rows exist. */
|
|
1409
549
|
async notExists() {
|
|
1410
550
|
return !(await this.exists());
|
|
1411
551
|
}
|
|
1412
|
-
/**
|
|
1413
|
-
* Count distinct values.
|
|
1414
|
-
*/
|
|
552
|
+
/** COUNT DISTINCT a field. */
|
|
1415
553
|
async countDistinct(field) {
|
|
1416
554
|
const result = await this.selectRaw(`COUNT(DISTINCT ${field}) as count`).first();
|
|
1417
555
|
return parseInt(result?.count ?? "0", 10);
|
|
1418
556
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
557
|
+
// ─── Aggregation shortcuts via latest/oldest ─────────────────
|
|
558
|
+
/** Get latest records ordered by a column. */
|
|
559
|
+
async latest(column = "createdAt") {
|
|
560
|
+
return this.orderBy(column, "desc").get();
|
|
561
|
+
}
|
|
562
|
+
// ─── Increment / Decrement ───────────────────────────────────
|
|
563
|
+
/** Increment a numeric field. Returns new value. */
|
|
1422
564
|
async increment(field, amount = 1) {
|
|
1423
565
|
this.applyPendingScopes();
|
|
1424
|
-
const { sql, params } = this.buildFilter();
|
|
1425
|
-
const updateSql = `UPDATE ${this.driver.dialect.quoteIdentifier(this.table)}
|
|
1426
|
-
|
|
566
|
+
const { sql: filterSql, params: filterParams } = this.buildFilter();
|
|
567
|
+
const updateSql = `UPDATE ${this.driver.dialect.quoteIdentifier(this.table)} ` +
|
|
568
|
+
`SET ${this.driver.dialect.quoteIdentifier(field)} = COALESCE(${this.driver.dialect.quoteIdentifier(field)}, 0) + $1 ` +
|
|
569
|
+
(filterSql ? `WHERE ${filterSql.replace("WHERE ", "")} ` : "") +
|
|
570
|
+
`RETURNING ${this.driver.dialect.quoteIdentifier(field)}`;
|
|
571
|
+
const result = await this.driver.query(updateSql, [
|
|
572
|
+
amount,
|
|
573
|
+
...filterParams,
|
|
574
|
+
]);
|
|
1427
575
|
return result.rows[0]?.[field] ?? 0;
|
|
1428
576
|
}
|
|
1429
|
-
/**
|
|
1430
|
-
* Decrement field value.
|
|
1431
|
-
*/
|
|
577
|
+
/** Decrement a numeric field. Returns new value. */
|
|
1432
578
|
async decrement(field, amount = 1) {
|
|
1433
579
|
return this.increment(field, -amount);
|
|
1434
580
|
}
|
|
1435
|
-
/**
|
|
1436
|
-
* Increment for all matching.
|
|
1437
|
-
*/
|
|
581
|
+
/** Increment a field for all matching rows. Returns affected row count. */
|
|
1438
582
|
async incrementMany(field, amount = 1) {
|
|
1439
583
|
this.applyPendingScopes();
|
|
1440
|
-
const { sql, params } = this.buildFilter();
|
|
1441
|
-
const updateSql = `UPDATE ${this.driver.dialect.quoteIdentifier(this.table)}
|
|
1442
|
-
|
|
584
|
+
const { sql: filterSql, params: filterParams } = this.buildFilter();
|
|
585
|
+
const updateSql = `UPDATE ${this.driver.dialect.quoteIdentifier(this.table)} ` +
|
|
586
|
+
`SET ${this.driver.dialect.quoteIdentifier(field)} = COALESCE(${this.driver.dialect.quoteIdentifier(field)}, 0) + $1` +
|
|
587
|
+
(filterSql ? ` WHERE ${filterSql.replace("WHERE ", "")}` : "");
|
|
588
|
+
const result = await this.driver.query(updateSql, [amount, ...filterParams]);
|
|
1443
589
|
return result.rowCount ?? 0;
|
|
1444
590
|
}
|
|
1445
|
-
/**
|
|
1446
|
-
* Decrement for all matching.
|
|
1447
|
-
*/
|
|
591
|
+
/** Decrement a field for all matching rows. Returns affected row count. */
|
|
1448
592
|
async decrementMany(field, amount = 1) {
|
|
1449
593
|
return this.incrementMany(field, -amount);
|
|
1450
594
|
}
|
|
1451
|
-
//
|
|
1452
|
-
// CHUNKING / PAGINATION
|
|
1453
|
-
// ============================================================================
|
|
595
|
+
// ─── Chunking / Pagination ───────────────────────────────────
|
|
1454
596
|
/**
|
|
1455
|
-
* Process results in chunks.
|
|
597
|
+
* Process results in memory-efficient chunks.
|
|
598
|
+
*
|
|
599
|
+
* @example
|
|
600
|
+
* await User.query().chunk(100, async (rows, idx) => { ... })
|
|
1456
601
|
*/
|
|
1457
602
|
async chunk(size, callback) {
|
|
1458
603
|
let chunkIndex = 0;
|
|
@@ -1462,20 +607,16 @@ class PostgresQueryBuilder {
|
|
|
1462
607
|
.skip(chunkIndex * size)
|
|
1463
608
|
.limit(size)
|
|
1464
609
|
.get();
|
|
1465
|
-
if (chunk.length === 0)
|
|
610
|
+
if (chunk.length === 0)
|
|
1466
611
|
break;
|
|
1467
|
-
}
|
|
1468
612
|
const shouldContinue = await callback(chunk, chunkIndex);
|
|
1469
|
-
if (shouldContinue === false)
|
|
613
|
+
if (shouldContinue === false)
|
|
1470
614
|
break;
|
|
1471
|
-
}
|
|
1472
615
|
hasMore = chunk.length === size;
|
|
1473
616
|
chunkIndex++;
|
|
1474
617
|
}
|
|
1475
618
|
}
|
|
1476
|
-
/**
|
|
1477
|
-
* Page-based pagination.
|
|
1478
|
-
*/
|
|
619
|
+
/** Page-based pagination. */
|
|
1479
620
|
async paginate(options) {
|
|
1480
621
|
const page = options?.page ?? 1;
|
|
1481
622
|
const limit = options?.limit ?? 10;
|
|
@@ -1495,22 +636,32 @@ class PostgresQueryBuilder {
|
|
|
1495
636
|
};
|
|
1496
637
|
}
|
|
1497
638
|
/**
|
|
1498
|
-
*
|
|
639
|
+
* Set cursor pagination hints fluently.
|
|
640
|
+
* The recorded values are picked up by `cursorPaginate()` when no explicit
|
|
641
|
+
* options are passed.
|
|
642
|
+
*
|
|
643
|
+
* @example
|
|
644
|
+
* User.query().cursor(lastId).cursorPaginate({ limit: 20 })
|
|
1499
645
|
*/
|
|
646
|
+
cursor(after, before) {
|
|
647
|
+
this.addOperation("cursor", { after, before });
|
|
648
|
+
return this;
|
|
649
|
+
}
|
|
650
|
+
/** Cursor-based pagination. */
|
|
1500
651
|
async cursorPaginate(options) {
|
|
1501
|
-
|
|
652
|
+
// Fall back to fluently-recorded cursor op if options.cursor not provided
|
|
653
|
+
const cursorOp = this.getOps("cursor")[0];
|
|
654
|
+
const recordedCursor = cursorOp?.data.after;
|
|
655
|
+
const { limit = 10, cursor = recordedCursor, column = "id", direction = "next", } = options ?? {};
|
|
1502
656
|
if (cursor) {
|
|
1503
|
-
|
|
1504
|
-
this.where(column, operator, cursor);
|
|
657
|
+
this.where(column, direction === "next" ? ">" : "<", cursor);
|
|
1505
658
|
}
|
|
1506
|
-
|
|
1507
|
-
this.orderBy(column, sortOrder);
|
|
659
|
+
this.orderBy(column, direction === "next" ? "asc" : "desc");
|
|
1508
660
|
const results = await this.limit(limit + 1).get();
|
|
1509
661
|
const hasMore = results.length > limit;
|
|
1510
662
|
let data = hasMore ? results.slice(0, limit) : results;
|
|
1511
|
-
if (direction === "prev")
|
|
663
|
+
if (direction === "prev")
|
|
1512
664
|
data = data.reverse();
|
|
1513
|
-
}
|
|
1514
665
|
let nextCursor;
|
|
1515
666
|
let prevCursor;
|
|
1516
667
|
let hasPrev = false;
|
|
@@ -1527,27 +678,14 @@ class PostgresQueryBuilder {
|
|
|
1527
678
|
else {
|
|
1528
679
|
prevCursor = hasMore ? firstItem : undefined;
|
|
1529
680
|
hasPrev = hasMore;
|
|
1530
|
-
if (cursor)
|
|
681
|
+
if (cursor)
|
|
1531
682
|
nextCursor = lastItem;
|
|
1532
|
-
}
|
|
1533
683
|
}
|
|
1534
684
|
}
|
|
1535
|
-
return {
|
|
1536
|
-
data,
|
|
1537
|
-
pagination: {
|
|
1538
|
-
hasMore,
|
|
1539
|
-
hasPrev,
|
|
1540
|
-
nextCursor,
|
|
1541
|
-
prevCursor,
|
|
1542
|
-
},
|
|
1543
|
-
};
|
|
685
|
+
return { data, pagination: { hasMore, hasPrev, nextCursor, prevCursor } };
|
|
1544
686
|
}
|
|
1545
|
-
//
|
|
1546
|
-
|
|
1547
|
-
// ============================================================================
|
|
1548
|
-
/**
|
|
1549
|
-
* Delete matching records.
|
|
1550
|
-
*/
|
|
687
|
+
// ─── Mutation methods ────────────────────────────────────────
|
|
688
|
+
/** Delete matching rows. Returns deleted count. */
|
|
1551
689
|
async delete() {
|
|
1552
690
|
this.applyPendingScopes();
|
|
1553
691
|
const { sql, params } = this.buildFilter();
|
|
@@ -1555,246 +693,241 @@ class PostgresQueryBuilder {
|
|
|
1555
693
|
const result = await this.driver.query(deleteSql, params);
|
|
1556
694
|
return result.rowCount ?? 0;
|
|
1557
695
|
}
|
|
1558
|
-
/**
|
|
1559
|
-
* Delete first matching record.
|
|
1560
|
-
*/
|
|
696
|
+
/** Delete the first matching row. */
|
|
1561
697
|
async deleteOne() {
|
|
1562
698
|
return this.limit(1).delete();
|
|
1563
699
|
}
|
|
1564
|
-
/**
|
|
1565
|
-
* Update matching records.
|
|
1566
|
-
*/
|
|
700
|
+
/** Update matching rows. */
|
|
1567
701
|
async update(fields) {
|
|
1568
702
|
this.applyPendingScopes();
|
|
1569
703
|
const result = await this.driver.updateMany(this.table, {}, { $set: fields });
|
|
1570
704
|
return result.modifiedCount;
|
|
1571
705
|
}
|
|
1572
|
-
/**
|
|
1573
|
-
* Unset fields from matching records.
|
|
1574
|
-
*/
|
|
706
|
+
/** Unset fields from matching rows. */
|
|
1575
707
|
async unset(...fields) {
|
|
1576
708
|
this.applyPendingScopes();
|
|
1577
709
|
const updateObj = {};
|
|
1578
|
-
for (const field of fields)
|
|
710
|
+
for (const field of fields)
|
|
1579
711
|
updateObj[field] = 1;
|
|
1580
|
-
}
|
|
1581
712
|
const result = await this.driver.updateMany(this.table, {}, { $unset: updateObj });
|
|
1582
713
|
return result.modifiedCount;
|
|
1583
714
|
}
|
|
1584
|
-
//
|
|
1585
|
-
|
|
1586
|
-
// ============================================================================
|
|
1587
|
-
/**
|
|
1588
|
-
* Get the raw SQL query.
|
|
1589
|
-
*/
|
|
715
|
+
// ─── Inspection / Debugging ───────────────────────────────────
|
|
716
|
+
/** Return the SQL + bindings without executing. */
|
|
1590
717
|
parse() {
|
|
1591
718
|
this.applyPendingScopes();
|
|
1592
719
|
const parser = new postgresQueryParser.PostgresQueryParser({
|
|
1593
720
|
table: this.table,
|
|
1594
|
-
operations: this.operations,
|
|
721
|
+
operations: toParserOps(this.operations),
|
|
1595
722
|
});
|
|
1596
723
|
return parser.parse();
|
|
1597
724
|
}
|
|
1598
|
-
/**
|
|
1599
|
-
* Get formatted SQL string.
|
|
1600
|
-
*/
|
|
725
|
+
/** Formatted SQL string (for logging/debugging). */
|
|
1601
726
|
pretty() {
|
|
1602
|
-
const {
|
|
1603
|
-
return `${
|
|
727
|
+
const { query = "", bindings } = this.parse();
|
|
728
|
+
return `${query}\n-- Bindings: ${JSON.stringify(bindings ?? [])}`;
|
|
1604
729
|
}
|
|
1605
|
-
/**
|
|
1606
|
-
* Get query execution plan.
|
|
1607
|
-
*/
|
|
730
|
+
/** Run EXPLAIN ANALYZE on the query. */
|
|
1608
731
|
async explain() {
|
|
1609
|
-
const {
|
|
1610
|
-
const
|
|
1611
|
-
const result = await this.driver.query(explainSql, params);
|
|
732
|
+
const { query = "", bindings = [] } = this.parse();
|
|
733
|
+
const result = await this.driver.query(`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${query}`, bindings);
|
|
1612
734
|
return result.rows;
|
|
1613
735
|
}
|
|
1614
|
-
//
|
|
1615
|
-
|
|
1616
|
-
// ============================================================================
|
|
1617
|
-
/**
|
|
1618
|
-
* Add driver-specific raw modifications to the query.
|
|
1619
|
-
*/
|
|
1620
|
-
raw(builder) {
|
|
1621
|
-
// For PostgreSQL, the native object would be the operations array
|
|
1622
|
-
builder(this.operations);
|
|
1623
|
-
return this;
|
|
1624
|
-
}
|
|
1625
|
-
/**
|
|
1626
|
-
* Extend the query builder with driver-specific extensions.
|
|
1627
|
-
*/
|
|
736
|
+
// ─── Utility ──────────────────────────────────────────────────
|
|
737
|
+
/** Extend the builder with a driver-specific extension. */
|
|
1628
738
|
extend(extension, ..._args) {
|
|
1629
|
-
// PostgreSQL doesn't have driver-specific extensions like MongoDB
|
|
1630
|
-
// Return undefined as R for now - specific extensions can be added later
|
|
1631
739
|
throw new Error(`Extension "${extension}" is not supported by PostgresQueryBuilder`);
|
|
1632
740
|
}
|
|
1633
|
-
/**
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
callback(this);
|
|
1638
|
-
return this;
|
|
1639
|
-
}
|
|
1640
|
-
/**
|
|
1641
|
-
* Conditionally apply query modifications.
|
|
1642
|
-
*/
|
|
1643
|
-
when(condition, callback, otherwise) {
|
|
1644
|
-
if (condition) {
|
|
1645
|
-
callback(this, condition);
|
|
1646
|
-
}
|
|
1647
|
-
else if (otherwise) {
|
|
1648
|
-
otherwise(this);
|
|
1649
|
-
}
|
|
1650
|
-
return this;
|
|
741
|
+
/** Pluck scalar values for a single field (alias for pluck). */
|
|
742
|
+
async pluckOne(field) {
|
|
743
|
+
const results = await this.select([field]).get();
|
|
744
|
+
return results.map((row) => row[field]);
|
|
1651
745
|
}
|
|
1652
746
|
// ============================================================================
|
|
1653
|
-
// INTERNAL
|
|
747
|
+
// JOIN RELATIONS — INTERNAL PIPELINE
|
|
1654
748
|
// ============================================================================
|
|
1655
749
|
/**
|
|
1656
|
-
*
|
|
750
|
+
* Before `get()` runs the parser, consume any joinWith ops recorded by the base
|
|
751
|
+
* class and expand them into the joinRelations Map.
|
|
1657
752
|
*/
|
|
1658
|
-
|
|
1659
|
-
const
|
|
1660
|
-
if (
|
|
1661
|
-
return
|
|
753
|
+
_processJoinWithOps() {
|
|
754
|
+
const joinWithOps = this.operations.filter((op) => op.type === "joinWith");
|
|
755
|
+
if (joinWithOps.length === 0)
|
|
756
|
+
return;
|
|
757
|
+
// Remove joinWith ops from main operations — they are consumed here
|
|
758
|
+
this.operations = this.operations.filter((op) => op.type !== "joinWith");
|
|
759
|
+
for (const op of joinWithOps) {
|
|
760
|
+
const constraints = op.data.constraints;
|
|
761
|
+
for (const [path, constraint] of Object.entries(constraints)) {
|
|
762
|
+
// Re-delegate to the extended joinWith implementation
|
|
763
|
+
if (!constraint || constraint === "") {
|
|
764
|
+
this.joinWith(path);
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
this.joinWith({ [path]: constraint });
|
|
768
|
+
}
|
|
769
|
+
}
|
|
1662
770
|
}
|
|
1663
|
-
const parser = new postgresQueryParser.PostgresQueryParser({
|
|
1664
|
-
table: this.table,
|
|
1665
|
-
operations: whereOps,
|
|
1666
|
-
});
|
|
1667
|
-
const { sql, params } = parser.parse();
|
|
1668
|
-
const whereMatch = sql.match(/WHERE .+$/);
|
|
1669
|
-
return {
|
|
1670
|
-
sql: whereMatch ? whereMatch[0] : "",
|
|
1671
|
-
params,
|
|
1672
|
-
};
|
|
1673
771
|
}
|
|
1674
|
-
// ============================================================================
|
|
1675
|
-
// RELATIONS / EAGER LOADING (Stubs)
|
|
1676
|
-
// ============================================================================
|
|
1677
|
-
/**
|
|
1678
|
-
* Relations to eagerly load.
|
|
1679
|
-
*/
|
|
1680
|
-
eagerLoadRelations = new Map();
|
|
1681
|
-
/**
|
|
1682
|
-
* Relations to count.
|
|
1683
|
-
*/
|
|
1684
|
-
countRelations = [];
|
|
1685
|
-
/**
|
|
1686
|
-
* Relations to load via JOIN (single query).
|
|
1687
|
-
*/
|
|
1688
|
-
joinRelations = new Map();
|
|
1689
|
-
/**
|
|
1690
|
-
* Relation definitions from the model.
|
|
1691
|
-
*/
|
|
1692
|
-
relationDefinitions;
|
|
1693
|
-
/**
|
|
1694
|
-
* Model class reference.
|
|
1695
|
-
*/
|
|
1696
|
-
modelClass;
|
|
1697
772
|
/**
|
|
1698
|
-
*
|
|
1699
|
-
*
|
|
1700
|
-
* Unlike `with()` which uses separate queries, `joinWith()` uses
|
|
1701
|
-
* LEFT JOIN to fetch related data in a single query.
|
|
1702
|
-
*
|
|
1703
|
-
* @param relations - Relation names to load via JOIN
|
|
1704
|
-
* @returns This builder for chaining
|
|
773
|
+
* Translate each entry in `joinRelations` into actual JOIN + selectRelatedColumns operations.
|
|
1705
774
|
*/
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
775
|
+
applyJoinRelations() {
|
|
776
|
+
if (this.joinRelations.size === 0)
|
|
777
|
+
return;
|
|
778
|
+
for (const [path, config] of this.joinRelations) {
|
|
779
|
+
const RelatedModel = typeof config.model === "string"
|
|
780
|
+
? registerModel.getModelFromRegistry(config.model)
|
|
781
|
+
: config.model;
|
|
782
|
+
if (!RelatedModel) {
|
|
783
|
+
throw new Error(`Relation model not found for ${path}`);
|
|
784
|
+
}
|
|
785
|
+
const relatedTable = RelatedModel.table;
|
|
786
|
+
const alias = config.alias;
|
|
787
|
+
const parentTable = config.parentPath
|
|
788
|
+
? this.joinRelations.get(config.parentPath).alias
|
|
789
|
+
: this.table;
|
|
790
|
+
const parentDefTable = config.parentModel?.table ?? this.table;
|
|
791
|
+
let localField;
|
|
792
|
+
let foreignField;
|
|
793
|
+
if (config.type === "belongsTo") {
|
|
794
|
+
localField = config.foreignKey ?? `${config.relationName}Id`;
|
|
795
|
+
foreignField = config.ownerKey ?? "id";
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
localField = config.localKey ?? "id";
|
|
799
|
+
foreignField = config.foreignKey ?? `${parentDefTable.slice(0, -1)}Id`;
|
|
800
|
+
}
|
|
801
|
+
// hasMany uses a correlated subquery in SELECT (no JOIN) to avoid row explosion
|
|
802
|
+
if (config.type !== "hasMany") {
|
|
803
|
+
this.addOperation("leftJoin", {
|
|
804
|
+
table: relatedTable,
|
|
805
|
+
alias,
|
|
806
|
+
localField: `${parentTable}.${localField}`,
|
|
807
|
+
foreignField,
|
|
1713
808
|
});
|
|
1714
809
|
}
|
|
810
|
+
this.addOperation("selectRelatedColumns", {
|
|
811
|
+
alias,
|
|
812
|
+
relationName: config.relationName,
|
|
813
|
+
path,
|
|
814
|
+
table: relatedTable,
|
|
815
|
+
select: config.select,
|
|
816
|
+
type: config.type,
|
|
817
|
+
foreignKey: foreignField,
|
|
818
|
+
localKey: localField,
|
|
819
|
+
parentTable,
|
|
820
|
+
constraintOps: config.constraintOps, // passed through to parser
|
|
821
|
+
});
|
|
1715
822
|
}
|
|
1716
|
-
return this;
|
|
1717
823
|
}
|
|
1718
824
|
/**
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1721
|
-
* Supported patterns:
|
|
1722
|
-
* - `with("posts")` - Load relation
|
|
1723
|
-
* - `with("posts", "comments")` - Load multiple relations
|
|
1724
|
-
* - `with("posts", callback)` - Load relation with constraint
|
|
1725
|
-
* - `with({ posts: true, comments: callback })` - Object configuration
|
|
1726
|
-
*
|
|
1727
|
-
* @param args - Relation name(s), callbacks, or configuration object
|
|
825
|
+
* Extract per-relation data from raw DB rows (before hydration).
|
|
826
|
+
* Returns a Map of row index → nested relation data tree.
|
|
1728
827
|
*/
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
828
|
+
extractJoinedRelationData(records) {
|
|
829
|
+
const result = new Map();
|
|
830
|
+
if (this.joinRelations.size === 0)
|
|
831
|
+
return result;
|
|
832
|
+
records.forEach((record, index) => {
|
|
833
|
+
const relationData = {};
|
|
834
|
+
// Process shallower paths first so parents exist before children
|
|
835
|
+
const sortedPaths = Array.from(this.joinRelations.keys()).sort((a, b) => a.split(".").length - b.split(".").length);
|
|
836
|
+
for (const path of sortedPaths) {
|
|
837
|
+
const config = this.joinRelations.get(path);
|
|
838
|
+
const columnName = config.alias;
|
|
839
|
+
const relatedData = record[columnName];
|
|
840
|
+
delete record[columnName];
|
|
841
|
+
const parsedData = relatedData !== null &&
|
|
842
|
+
!(typeof relatedData === "object" &&
|
|
843
|
+
Object.values(relatedData).every((v) => v === null))
|
|
844
|
+
? relatedData
|
|
845
|
+
: null;
|
|
846
|
+
const parts = path.split(".");
|
|
847
|
+
const lastPart = parts.pop();
|
|
848
|
+
let current = relationData;
|
|
849
|
+
for (const part of parts) {
|
|
850
|
+
if (!current[part])
|
|
851
|
+
current[part] = {};
|
|
852
|
+
current = current[part];
|
|
1746
853
|
}
|
|
854
|
+
current[lastPart] = parsedData;
|
|
1747
855
|
}
|
|
1748
|
-
|
|
1749
|
-
}
|
|
1750
|
-
return
|
|
1751
|
-
}
|
|
1752
|
-
/**
|
|
1753
|
-
* Add a count of related models as a virtual field.
|
|
1754
|
-
* @param relations - Relation name(s) to count
|
|
1755
|
-
*/
|
|
1756
|
-
withCount(...relations) {
|
|
1757
|
-
this.countRelations.push(...relations);
|
|
1758
|
-
return this;
|
|
1759
|
-
}
|
|
1760
|
-
/**
|
|
1761
|
-
* Filter results to only those that have related models.
|
|
1762
|
-
* @param relation - Relation name
|
|
1763
|
-
* @param operator - Optional comparison operator
|
|
1764
|
-
* @param count - Optional count to compare against
|
|
1765
|
-
*/
|
|
1766
|
-
has(relation, operator, count) {
|
|
1767
|
-
// TODO: Implement has() using EXISTS subquery
|
|
1768
|
-
this.addOperation("has", { relation, operator, count });
|
|
1769
|
-
return this;
|
|
1770
|
-
}
|
|
1771
|
-
/**
|
|
1772
|
-
* Filter results that have related models matching specific conditions.
|
|
1773
|
-
* @param relation - Relation name
|
|
1774
|
-
* @param callback - Callback to define conditions
|
|
1775
|
-
*/
|
|
1776
|
-
whereHas(relation, callback) {
|
|
1777
|
-
// TODO: Implement whereHas() using EXISTS subquery with conditions
|
|
1778
|
-
this.addOperation("whereHas", { relation, callback });
|
|
1779
|
-
return this;
|
|
856
|
+
result.set(index, relationData);
|
|
857
|
+
});
|
|
858
|
+
return result;
|
|
1780
859
|
}
|
|
1781
860
|
/**
|
|
1782
|
-
*
|
|
1783
|
-
* @param relation - Relation name
|
|
861
|
+
* Attach extracted relation data to hydrated model instances.
|
|
1784
862
|
*/
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
863
|
+
attachJoinedRelations(records, joinedData) {
|
|
864
|
+
if (this.joinRelations.size === 0)
|
|
865
|
+
return;
|
|
866
|
+
const attachNested = (model, dataTree, currentPath = "") => {
|
|
867
|
+
if (!dataTree || typeof dataTree !== "object")
|
|
868
|
+
return;
|
|
869
|
+
for (const [key, data] of Object.entries(dataTree)) {
|
|
870
|
+
const path = currentPath ? `${currentPath}.${key}` : key;
|
|
871
|
+
const config = this.joinRelations.get(path);
|
|
872
|
+
if (!config)
|
|
873
|
+
continue;
|
|
874
|
+
const m = model;
|
|
875
|
+
if (data === null) {
|
|
876
|
+
m[key] = null;
|
|
877
|
+
m.loadedRelations?.set(key, null);
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
const RelatedModel = registerModel.resolveModelClass(config.model);
|
|
881
|
+
if (!RelatedModel)
|
|
882
|
+
continue;
|
|
883
|
+
const childKeys = Array.from(this.joinRelations.keys())
|
|
884
|
+
.filter((p) => p.startsWith(`${path}.`))
|
|
885
|
+
.map((p) => p.split(".")[path.split(".").length]);
|
|
886
|
+
if (config.type === "hasMany") {
|
|
887
|
+
const rows = Array.isArray(data) ? data : [];
|
|
888
|
+
const instances = rows.map((row) => {
|
|
889
|
+
const rowData = { ...row };
|
|
890
|
+
for (const childKey of childKeys)
|
|
891
|
+
delete rowData[childKey];
|
|
892
|
+
return RelatedModel.hydrate(rowData);
|
|
893
|
+
});
|
|
894
|
+
m[key] = instances;
|
|
895
|
+
m.loadedRelations?.set(key, instances);
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
const modelData = { ...data };
|
|
899
|
+
for (const childKey of childKeys)
|
|
900
|
+
delete modelData[childKey];
|
|
901
|
+
const relatedInstance = RelatedModel.hydrate(modelData);
|
|
902
|
+
attachNested(relatedInstance, data, path);
|
|
903
|
+
m[key] = relatedInstance;
|
|
904
|
+
m.loadedRelations?.set(key, relatedInstance);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
records.forEach((model, index) => {
|
|
909
|
+
const relationData = joinedData.get(index);
|
|
910
|
+
if (relationData)
|
|
911
|
+
attachNested(model, relationData);
|
|
912
|
+
});
|
|
1789
913
|
}
|
|
914
|
+
// ============================================================================
|
|
915
|
+
// INTERNAL HELPERS
|
|
916
|
+
// ============================================================================
|
|
1790
917
|
/**
|
|
1791
|
-
*
|
|
1792
|
-
*
|
|
1793
|
-
* @param callback - Callback to define conditions
|
|
918
|
+
* Build a WHERE-only SQL fragment from `where*` operations on the current builder.
|
|
919
|
+
* Used by DELETE / UPDATE / increment paths.
|
|
1794
920
|
*/
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
921
|
+
buildFilter() {
|
|
922
|
+
const whereOps = this.operations.filter((op) => op.type.includes("where") || op.type.includes("Where"));
|
|
923
|
+
if (whereOps.length === 0)
|
|
924
|
+
return { sql: "", params: [] };
|
|
925
|
+
const parser = new postgresQueryParser.PostgresQueryParser({
|
|
926
|
+
table: this.table,
|
|
927
|
+
operations: toParserOps(whereOps),
|
|
928
|
+
});
|
|
929
|
+
const { query = "", bindings = [] } = parser.parse();
|
|
930
|
+
const whereMatch = query.match(/WHERE .+$/);
|
|
931
|
+
return { sql: whereMatch ? whereMatch[0] : "", params: bindings };
|
|
1799
932
|
}
|
|
1800
933
|
}exports.PostgresQueryBuilder=PostgresQueryBuilder;//# sourceMappingURL=postgres-query-builder.js.map
|