@warlock.js/cascade 4.0.92 → 4.0.93
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/contracts/database-driver.contract.d.ts +118 -0
- package/cjs/contracts/database-driver.contract.d.ts.map +1 -1
- package/cjs/contracts/migration-driver.contract.d.ts +14 -0
- package/cjs/contracts/migration-driver.contract.d.ts.map +1 -1
- package/cjs/contracts/query-builder.contract.d.ts +410 -1
- package/cjs/contracts/query-builder.contract.d.ts.map +1 -1
- package/cjs/data-source/data-source-registry.d.ts +4 -0
- package/cjs/data-source/data-source-registry.d.ts.map +1 -1
- package/cjs/data-source/data-source-registry.js +7 -0
- package/cjs/data-source/data-source-registry.js.map +1 -1
- package/cjs/drivers/mongodb/mongodb-blueprint.d.ts.map +1 -0
- package/cjs/drivers/mongodb/mongodb-blueprint.js.map +1 -0
- package/{esm/drivers/mongo → cjs/drivers/mongodb}/mongodb-driver.d.ts +49 -0
- package/cjs/drivers/mongodb/mongodb-driver.d.ts.map +1 -0
- package/cjs/drivers/{mongo → mongodb}/mongodb-driver.js +125 -8
- package/cjs/drivers/mongodb/mongodb-driver.js.map +1 -0
- package/cjs/drivers/{mongo/mongo-id-generator.d.ts → mongodb/mongodb-id-generator.d.ts} +1 -1
- package/cjs/drivers/mongodb/mongodb-id-generator.d.ts.map +1 -0
- package/cjs/drivers/{mongo/mongo-id-generator.js → mongodb/mongodb-id-generator.js} +1 -1
- package/cjs/drivers/mongodb/mongodb-id-generator.js.map +1 -0
- package/cjs/drivers/{mongo/mongo-migration-driver.d.ts → mongodb/mongodb-migration-driver.d.ts} +10 -1
- package/cjs/drivers/mongodb/mongodb-migration-driver.d.ts.map +1 -0
- package/cjs/drivers/{mongo/mongo-migration-driver.js → mongodb/mongodb-migration-driver.js} +18 -1
- package/cjs/drivers/mongodb/mongodb-migration-driver.js.map +1 -0
- package/cjs/drivers/{mongo/mongo-query-builder.d.ts → mongodb/mongodb-query-builder.d.ts} +172 -4
- package/cjs/drivers/mongodb/mongodb-query-builder.d.ts.map +1 -0
- package/cjs/drivers/{mongo/mongo-query-builder.js → mongodb/mongodb-query-builder.js} +220 -14
- package/cjs/drivers/mongodb/mongodb-query-builder.js.map +1 -0
- package/{esm/drivers/mongo/mongo-query-operations.d.ts → cjs/drivers/mongodb/mongodb-query-operations.d.ts} +16 -16
- package/cjs/drivers/mongodb/mongodb-query-operations.d.ts.map +1 -0
- package/cjs/drivers/{mongo/mongo-query-operations.js → mongodb/mongodb-query-operations.js} +22 -22
- package/cjs/drivers/mongodb/mongodb-query-operations.js.map +1 -0
- package/cjs/drivers/{mongo/mongo-query-parser.d.ts → mongodb/mongodb-query-parser.d.ts} +2 -2
- package/cjs/drivers/mongodb/mongodb-query-parser.d.ts.map +1 -0
- package/cjs/drivers/{mongo/mongo-query-parser.js → mongodb/mongodb-query-parser.js} +87 -87
- package/cjs/drivers/mongodb/mongodb-query-parser.js.map +1 -0
- package/cjs/drivers/{mongo/mongo-sync-adapter.d.ts → mongodb/mongodb-sync-adapter.d.ts} +2 -2
- package/cjs/drivers/mongodb/mongodb-sync-adapter.d.ts.map +1 -0
- package/cjs/drivers/{mongo/mongo-sync-adapter.js → mongodb/mongodb-sync-adapter.js} +2 -2
- package/cjs/drivers/mongodb/mongodb-sync-adapter.js.map +1 -0
- package/{esm/drivers/mongo → cjs/drivers/mongodb}/types.d.ts +2 -2
- package/cjs/drivers/mongodb/types.d.ts.map +1 -0
- package/cjs/drivers/postgres/index.d.ts +16 -0
- package/cjs/drivers/postgres/index.d.ts.map +1 -0
- package/cjs/drivers/postgres/postgres-blueprint.d.ts +64 -0
- package/cjs/drivers/postgres/postgres-blueprint.d.ts.map +1 -0
- package/cjs/drivers/postgres/postgres-blueprint.js +121 -0
- package/cjs/drivers/postgres/postgres-blueprint.js.map +1 -0
- package/cjs/drivers/postgres/postgres-dialect.d.ts +135 -0
- package/cjs/drivers/postgres/postgres-dialect.d.ts.map +1 -0
- package/cjs/drivers/postgres/postgres-dialect.js +245 -0
- package/cjs/drivers/postgres/postgres-dialect.js.map +1 -0
- package/cjs/drivers/postgres/postgres-driver.d.ts +360 -0
- package/cjs/drivers/postgres/postgres-driver.d.ts.map +1 -0
- package/cjs/drivers/postgres/postgres-driver.js +763 -0
- package/cjs/drivers/postgres/postgres-driver.js.map +1 -0
- package/cjs/drivers/postgres/postgres-migration-driver.d.ts +297 -0
- package/cjs/drivers/postgres/postgres-migration-driver.d.ts.map +1 -0
- package/cjs/drivers/postgres/postgres-migration-driver.js +578 -0
- package/cjs/drivers/postgres/postgres-migration-driver.js.map +1 -0
- package/cjs/drivers/postgres/postgres-query-builder.d.ts +824 -0
- package/cjs/drivers/postgres/postgres-query-builder.d.ts.map +1 -0
- package/cjs/drivers/postgres/postgres-query-builder.js +1800 -0
- package/cjs/drivers/postgres/postgres-query-builder.js.map +1 -0
- package/cjs/drivers/postgres/postgres-query-parser.d.ts +308 -0
- package/cjs/drivers/postgres/postgres-query-parser.d.ts.map +1 -0
- package/cjs/drivers/postgres/postgres-query-parser.js +706 -0
- package/cjs/drivers/postgres/postgres-query-parser.js.map +1 -0
- package/cjs/drivers/postgres/postgres-sync-adapter.d.ts +83 -0
- package/cjs/drivers/postgres/postgres-sync-adapter.d.ts.map +1 -0
- package/cjs/drivers/postgres/postgres-sync-adapter.js +197 -0
- package/cjs/drivers/postgres/postgres-sync-adapter.js.map +1 -0
- package/cjs/drivers/postgres/types.d.ts +142 -0
- package/cjs/drivers/postgres/types.d.ts.map +1 -0
- package/cjs/drivers/sql/index.d.ts +10 -0
- package/cjs/drivers/sql/index.d.ts.map +1 -0
- package/cjs/drivers/sql/sql-dialect.contract.d.ts +203 -0
- package/cjs/drivers/sql/sql-dialect.contract.d.ts.map +1 -0
- package/cjs/drivers/sql/sql-types.d.ts +202 -0
- package/cjs/drivers/sql/sql-types.d.ts.map +1 -0
- package/cjs/index.d.ts +9 -6
- package/cjs/index.d.ts.map +1 -1
- package/cjs/index.js +1 -1
- package/cjs/migration/migration-runner.d.ts.map +1 -1
- package/cjs/migration/migration-runner.js +3 -0
- package/cjs/migration/migration-runner.js.map +1 -1
- package/cjs/model/model.d.ts +236 -1
- package/cjs/model/model.d.ts.map +1 -1
- package/cjs/model/model.js +203 -4
- package/cjs/model/model.js.map +1 -1
- package/cjs/relations/helpers.d.ts +156 -0
- package/cjs/relations/helpers.d.ts.map +1 -0
- package/cjs/relations/helpers.js +197 -0
- package/cjs/relations/helpers.js.map +1 -0
- package/cjs/relations/index.d.ts +33 -0
- package/cjs/relations/index.d.ts.map +1 -0
- package/cjs/relations/pivot-operations.d.ts +160 -0
- package/cjs/relations/pivot-operations.d.ts.map +1 -0
- package/cjs/relations/pivot-operations.js +293 -0
- package/cjs/relations/pivot-operations.js.map +1 -0
- package/cjs/relations/relation-loader.d.ts +194 -0
- package/cjs/relations/relation-loader.d.ts.map +1 -0
- package/cjs/relations/relation-loader.js +466 -0
- package/cjs/relations/relation-loader.js.map +1 -0
- package/cjs/relations/types.d.ts +280 -0
- package/cjs/relations/types.d.ts.map +1 -0
- package/cjs/sync/model-sync-operation.js +1 -1
- package/cjs/sync/model-sync-operation.js.map +1 -1
- package/cjs/utils/connect-to-database.d.ts.map +1 -1
- package/cjs/utils/connect-to-database.js +15 -3
- package/cjs/utils/connect-to-database.js.map +1 -1
- package/cjs/utils/define-model.d.ts +51 -29
- package/cjs/utils/define-model.d.ts.map +1 -1
- package/cjs/validation/rules/database-model-rule.js +1 -1
- package/cjs/validation/rules/database-model-rule.js.map +1 -1
- package/esm/contracts/database-driver.contract.d.ts +118 -0
- package/esm/contracts/database-driver.contract.d.ts.map +1 -1
- package/esm/contracts/migration-driver.contract.d.ts +14 -0
- package/esm/contracts/migration-driver.contract.d.ts.map +1 -1
- package/esm/contracts/query-builder.contract.d.ts +410 -1
- package/esm/contracts/query-builder.contract.d.ts.map +1 -1
- package/esm/data-source/data-source-registry.d.ts +4 -0
- package/esm/data-source/data-source-registry.d.ts.map +1 -1
- package/esm/data-source/data-source-registry.js +7 -0
- package/esm/data-source/data-source-registry.js.map +1 -1
- package/esm/drivers/mongodb/mongodb-blueprint.d.ts.map +1 -0
- package/esm/drivers/mongodb/mongodb-blueprint.js.map +1 -0
- package/{cjs/drivers/mongo → esm/drivers/mongodb}/mongodb-driver.d.ts +49 -0
- package/esm/drivers/mongodb/mongodb-driver.d.ts.map +1 -0
- package/esm/drivers/{mongo → mongodb}/mongodb-driver.js +121 -4
- package/esm/drivers/mongodb/mongodb-driver.js.map +1 -0
- package/esm/drivers/{mongo/mongo-id-generator.d.ts → mongodb/mongodb-id-generator.d.ts} +1 -1
- package/esm/drivers/mongodb/mongodb-id-generator.d.ts.map +1 -0
- package/esm/drivers/{mongo/mongo-id-generator.js → mongodb/mongodb-id-generator.js} +1 -1
- package/esm/drivers/mongodb/mongodb-id-generator.js.map +1 -0
- package/esm/drivers/{mongo/mongo-migration-driver.d.ts → mongodb/mongodb-migration-driver.d.ts} +10 -1
- package/esm/drivers/mongodb/mongodb-migration-driver.d.ts.map +1 -0
- package/esm/drivers/{mongo/mongo-migration-driver.js → mongodb/mongodb-migration-driver.js} +18 -1
- package/esm/drivers/mongodb/mongodb-migration-driver.js.map +1 -0
- package/esm/drivers/{mongo/mongo-query-builder.d.ts → mongodb/mongodb-query-builder.d.ts} +172 -4
- package/esm/drivers/mongodb/mongodb-query-builder.d.ts.map +1 -0
- package/esm/drivers/{mongo/mongo-query-builder.js → mongodb/mongodb-query-builder.js} +218 -12
- package/esm/drivers/mongodb/mongodb-query-builder.js.map +1 -0
- package/{cjs/drivers/mongo/mongo-query-operations.d.ts → esm/drivers/mongodb/mongodb-query-operations.d.ts} +16 -16
- package/esm/drivers/mongodb/mongodb-query-operations.d.ts.map +1 -0
- package/esm/drivers/{mongo/mongo-query-operations.js → mongodb/mongodb-query-operations.js} +22 -22
- package/esm/drivers/mongodb/mongodb-query-operations.js.map +1 -0
- package/esm/drivers/{mongo/mongo-query-parser.d.ts → mongodb/mongodb-query-parser.d.ts} +2 -2
- package/esm/drivers/mongodb/mongodb-query-parser.d.ts.map +1 -0
- package/esm/drivers/{mongo/mongo-query-parser.js → mongodb/mongodb-query-parser.js} +87 -87
- package/esm/drivers/mongodb/mongodb-query-parser.js.map +1 -0
- package/esm/drivers/{mongo/mongo-sync-adapter.d.ts → mongodb/mongodb-sync-adapter.d.ts} +2 -2
- package/esm/drivers/mongodb/mongodb-sync-adapter.d.ts.map +1 -0
- package/esm/drivers/{mongo/mongo-sync-adapter.js → mongodb/mongodb-sync-adapter.js} +2 -2
- package/esm/drivers/mongodb/mongodb-sync-adapter.js.map +1 -0
- package/{cjs/drivers/mongo → esm/drivers/mongodb}/types.d.ts +2 -2
- package/esm/drivers/mongodb/types.d.ts.map +1 -0
- package/esm/drivers/postgres/index.d.ts +16 -0
- package/esm/drivers/postgres/index.d.ts.map +1 -0
- package/esm/drivers/postgres/postgres-blueprint.d.ts +64 -0
- package/esm/drivers/postgres/postgres-blueprint.d.ts.map +1 -0
- package/esm/drivers/postgres/postgres-blueprint.js +121 -0
- package/esm/drivers/postgres/postgres-blueprint.js.map +1 -0
- package/esm/drivers/postgres/postgres-dialect.d.ts +135 -0
- package/esm/drivers/postgres/postgres-dialect.d.ts.map +1 -0
- package/esm/drivers/postgres/postgres-dialect.js +245 -0
- package/esm/drivers/postgres/postgres-dialect.js.map +1 -0
- package/esm/drivers/postgres/postgres-driver.d.ts +360 -0
- package/esm/drivers/postgres/postgres-driver.d.ts.map +1 -0
- package/esm/drivers/postgres/postgres-driver.js +763 -0
- package/esm/drivers/postgres/postgres-driver.js.map +1 -0
- package/esm/drivers/postgres/postgres-migration-driver.d.ts +297 -0
- package/esm/drivers/postgres/postgres-migration-driver.d.ts.map +1 -0
- package/esm/drivers/postgres/postgres-migration-driver.js +578 -0
- package/esm/drivers/postgres/postgres-migration-driver.js.map +1 -0
- package/esm/drivers/postgres/postgres-query-builder.d.ts +824 -0
- package/esm/drivers/postgres/postgres-query-builder.d.ts.map +1 -0
- package/esm/drivers/postgres/postgres-query-builder.js +1800 -0
- package/esm/drivers/postgres/postgres-query-builder.js.map +1 -0
- package/esm/drivers/postgres/postgres-query-parser.d.ts +308 -0
- package/esm/drivers/postgres/postgres-query-parser.d.ts.map +1 -0
- package/esm/drivers/postgres/postgres-query-parser.js +706 -0
- package/esm/drivers/postgres/postgres-query-parser.js.map +1 -0
- package/esm/drivers/postgres/postgres-sync-adapter.d.ts +83 -0
- package/esm/drivers/postgres/postgres-sync-adapter.d.ts.map +1 -0
- package/esm/drivers/postgres/postgres-sync-adapter.js +197 -0
- package/esm/drivers/postgres/postgres-sync-adapter.js.map +1 -0
- package/esm/drivers/postgres/types.d.ts +142 -0
- package/esm/drivers/postgres/types.d.ts.map +1 -0
- package/esm/drivers/sql/index.d.ts +10 -0
- package/esm/drivers/sql/index.d.ts.map +1 -0
- package/esm/drivers/sql/sql-dialect.contract.d.ts +203 -0
- package/esm/drivers/sql/sql-dialect.contract.d.ts.map +1 -0
- package/esm/drivers/sql/sql-types.d.ts +202 -0
- package/esm/drivers/sql/sql-types.d.ts.map +1 -0
- package/esm/index.d.ts +9 -6
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +1 -1
- package/esm/migration/migration-runner.d.ts.map +1 -1
- package/esm/migration/migration-runner.js +3 -0
- package/esm/migration/migration-runner.js.map +1 -1
- package/esm/model/model.d.ts +236 -1
- package/esm/model/model.d.ts.map +1 -1
- package/esm/model/model.js +203 -4
- package/esm/model/model.js.map +1 -1
- package/esm/relations/helpers.d.ts +156 -0
- package/esm/relations/helpers.d.ts.map +1 -0
- package/esm/relations/helpers.js +197 -0
- package/esm/relations/helpers.js.map +1 -0
- package/esm/relations/index.d.ts +33 -0
- package/esm/relations/index.d.ts.map +1 -0
- package/esm/relations/pivot-operations.d.ts +160 -0
- package/esm/relations/pivot-operations.d.ts.map +1 -0
- package/esm/relations/pivot-operations.js +293 -0
- package/esm/relations/pivot-operations.js.map +1 -0
- package/esm/relations/relation-loader.d.ts +194 -0
- package/esm/relations/relation-loader.d.ts.map +1 -0
- package/esm/relations/relation-loader.js +466 -0
- package/esm/relations/relation-loader.js.map +1 -0
- package/esm/relations/types.d.ts +280 -0
- package/esm/relations/types.d.ts.map +1 -0
- package/esm/sync/model-sync-operation.js +1 -1
- package/esm/sync/model-sync-operation.js.map +1 -1
- package/esm/utils/connect-to-database.d.ts.map +1 -1
- package/esm/utils/connect-to-database.js +15 -3
- package/esm/utils/connect-to-database.js.map +1 -1
- package/esm/utils/define-model.d.ts +51 -29
- package/esm/utils/define-model.d.ts.map +1 -1
- package/esm/validation/rules/database-model-rule.js +1 -1
- package/esm/validation/rules/database-model-rule.js.map +1 -1
- package/package.json +4 -4
- package/cjs/drivers/mongo/mongo-id-generator.d.ts.map +0 -1
- package/cjs/drivers/mongo/mongo-id-generator.js.map +0 -1
- package/cjs/drivers/mongo/mongo-migration-driver.d.ts.map +0 -1
- package/cjs/drivers/mongo/mongo-migration-driver.js.map +0 -1
- package/cjs/drivers/mongo/mongo-query-builder.d.ts.map +0 -1
- package/cjs/drivers/mongo/mongo-query-builder.js.map +0 -1
- package/cjs/drivers/mongo/mongo-query-operations.d.ts.map +0 -1
- package/cjs/drivers/mongo/mongo-query-operations.js.map +0 -1
- package/cjs/drivers/mongo/mongo-query-parser.d.ts.map +0 -1
- package/cjs/drivers/mongo/mongo-query-parser.js.map +0 -1
- package/cjs/drivers/mongo/mongo-sync-adapter.d.ts.map +0 -1
- package/cjs/drivers/mongo/mongo-sync-adapter.js.map +0 -1
- package/cjs/drivers/mongo/mongodb-blueprint.d.ts.map +0 -1
- package/cjs/drivers/mongo/mongodb-blueprint.js.map +0 -1
- package/cjs/drivers/mongo/mongodb-driver.d.ts.map +0 -1
- package/cjs/drivers/mongo/mongodb-driver.js.map +0 -1
- package/cjs/drivers/mongo/types.d.ts.map +0 -1
- package/esm/drivers/mongo/mongo-id-generator.d.ts.map +0 -1
- package/esm/drivers/mongo/mongo-id-generator.js.map +0 -1
- package/esm/drivers/mongo/mongo-migration-driver.d.ts.map +0 -1
- package/esm/drivers/mongo/mongo-migration-driver.js.map +0 -1
- package/esm/drivers/mongo/mongo-query-builder.d.ts.map +0 -1
- package/esm/drivers/mongo/mongo-query-builder.js.map +0 -1
- package/esm/drivers/mongo/mongo-query-operations.d.ts.map +0 -1
- package/esm/drivers/mongo/mongo-query-operations.js.map +0 -1
- package/esm/drivers/mongo/mongo-query-parser.d.ts.map +0 -1
- package/esm/drivers/mongo/mongo-query-parser.js.map +0 -1
- package/esm/drivers/mongo/mongo-sync-adapter.d.ts.map +0 -1
- package/esm/drivers/mongo/mongo-sync-adapter.js.map +0 -1
- package/esm/drivers/mongo/mongodb-blueprint.d.ts.map +0 -1
- package/esm/drivers/mongo/mongodb-blueprint.js.map +0 -1
- package/esm/drivers/mongo/mongodb-driver.d.ts.map +0 -1
- package/esm/drivers/mongo/mongodb-driver.js.map +0 -1
- package/esm/drivers/mongo/types.d.ts.map +0 -1
- /package/cjs/drivers/{mongo → mongodb}/mongodb-blueprint.d.ts +0 -0
- /package/cjs/drivers/{mongo → mongodb}/mongodb-blueprint.js +0 -0
- /package/esm/drivers/{mongo → mongodb}/mongodb-blueprint.d.ts +0 -0
- /package/esm/drivers/{mongo → mongodb}/mongodb-blueprint.js +0 -0
|
@@ -0,0 +1,1800 @@
|
|
|
1
|
+
import {dataSourceRegistry}from'../../data-source/data-source-registry.js';import {getModelFromRegistry}from'../../model/register-model.js';import {PostgresQueryParser}from'./postgres-query-parser.js';/**
|
|
2
|
+
* PostgreSQL Query Builder
|
|
3
|
+
*
|
|
4
|
+
* Implements the QueryBuilderContract for PostgreSQL databases.
|
|
5
|
+
* Provides a fluent API for building SQL queries with proper
|
|
6
|
+
* parameter handling and type safety.
|
|
7
|
+
*
|
|
8
|
+
* @module cascade/drivers/postgres
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* PostgreSQL Query Builder.
|
|
12
|
+
*
|
|
13
|
+
* Implements the Cascade QueryBuilderContract for PostgreSQL.
|
|
14
|
+
* Collects query operations and delegates to PostgresQueryParser
|
|
15
|
+
* for SQL generation.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const users = await queryBuilder('users')
|
|
20
|
+
* .select(['id', 'name', 'email'])
|
|
21
|
+
* .where('status', 'active')
|
|
22
|
+
* .where('age', '>', 18)
|
|
23
|
+
* .orderBy('createdAt', 'desc')
|
|
24
|
+
* .limit(10)
|
|
25
|
+
* .get();
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
class PostgresQueryBuilder {
|
|
29
|
+
table;
|
|
30
|
+
/**
|
|
31
|
+
* Collected operations to be parsed into SQL.
|
|
32
|
+
*/
|
|
33
|
+
operations = [];
|
|
34
|
+
/**
|
|
35
|
+
* Data source instance.
|
|
36
|
+
*/
|
|
37
|
+
dataSource;
|
|
38
|
+
/**
|
|
39
|
+
* Hydrate callback for transforming results.
|
|
40
|
+
*/
|
|
41
|
+
hydrateCallback;
|
|
42
|
+
/**
|
|
43
|
+
* Callback invoked before query execution.
|
|
44
|
+
*/
|
|
45
|
+
fetchingCallback;
|
|
46
|
+
/**
|
|
47
|
+
* Callback invoked after records fetched but before hydration.
|
|
48
|
+
*/
|
|
49
|
+
hydratingCallback;
|
|
50
|
+
/**
|
|
51
|
+
* Callback invoked after records fetched and hydrated.
|
|
52
|
+
*/
|
|
53
|
+
fetchedCallback;
|
|
54
|
+
/**
|
|
55
|
+
* Pending global scopes.
|
|
56
|
+
*/
|
|
57
|
+
pendingGlobalScopes;
|
|
58
|
+
/**
|
|
59
|
+
* Available local scopes.
|
|
60
|
+
*/
|
|
61
|
+
availableLocalScopes;
|
|
62
|
+
/**
|
|
63
|
+
* Disabled global scope names.
|
|
64
|
+
*/
|
|
65
|
+
disabledGlobalScopes = new Set();
|
|
66
|
+
/**
|
|
67
|
+
* Whether scopes have been applied.
|
|
68
|
+
*/
|
|
69
|
+
scopesApplied = false;
|
|
70
|
+
/**
|
|
71
|
+
* Create a new query builder.
|
|
72
|
+
*
|
|
73
|
+
* @param table - Target table name
|
|
74
|
+
* @param dataSource - Optional data source (uses default if not provided)
|
|
75
|
+
*/
|
|
76
|
+
constructor(table, dataSource) {
|
|
77
|
+
this.table = table;
|
|
78
|
+
this.dataSource = dataSource ?? dataSourceRegistry.get();
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get the PostgreSQL driver instance.
|
|
82
|
+
*/
|
|
83
|
+
get driver() {
|
|
84
|
+
return this.dataSource.driver;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Add an operation to the operations list.
|
|
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
|
+
*/
|
|
100
|
+
clone() {
|
|
101
|
+
const cloned = new PostgresQueryBuilder(this.table, this.dataSource);
|
|
102
|
+
cloned.operations = [...this.operations];
|
|
103
|
+
cloned.hydrateCallback = this.hydrateCallback;
|
|
104
|
+
cloned.pendingGlobalScopes = this.pendingGlobalScopes;
|
|
105
|
+
cloned.availableLocalScopes = this.availableLocalScopes;
|
|
106
|
+
cloned.disabledGlobalScopes = new Set(this.disabledGlobalScopes);
|
|
107
|
+
cloned.scopesApplied = this.scopesApplied;
|
|
108
|
+
return cloned;
|
|
109
|
+
}
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// HYDRATION
|
|
112
|
+
// ============================================================================
|
|
113
|
+
/**
|
|
114
|
+
* Set a hydration callback to transform each result row.
|
|
115
|
+
*
|
|
116
|
+
* @param callback - Transform function
|
|
117
|
+
* @returns This builder for chaining
|
|
118
|
+
*/
|
|
119
|
+
hydrate(callback) {
|
|
120
|
+
this.hydrateCallback = callback;
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Register callback invoked before query execution.
|
|
125
|
+
*
|
|
126
|
+
* @param callback - Callback function
|
|
127
|
+
* @returns Unsubscribe function
|
|
128
|
+
*/
|
|
129
|
+
onFetching(callback) {
|
|
130
|
+
this.fetchingCallback = callback;
|
|
131
|
+
return () => {
|
|
132
|
+
this.fetchingCallback = undefined;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Register callback invoked after fetch but before hydration.
|
|
137
|
+
*
|
|
138
|
+
* @param callback - Callback function
|
|
139
|
+
* @returns Unsubscribe function
|
|
140
|
+
*/
|
|
141
|
+
onHydrating(callback) {
|
|
142
|
+
this.hydratingCallback = callback;
|
|
143
|
+
return () => {
|
|
144
|
+
this.hydratingCallback = undefined;
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Register callback invoked after fetch and hydration.
|
|
149
|
+
*
|
|
150
|
+
* @param callback - Callback function
|
|
151
|
+
* @returns Unsubscribe function
|
|
152
|
+
*/
|
|
153
|
+
onFetched(callback) {
|
|
154
|
+
this.fetchedCallback = callback;
|
|
155
|
+
return () => {
|
|
156
|
+
this.fetchedCallback = undefined;
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// ============================================================================
|
|
160
|
+
// SCOPES
|
|
161
|
+
// ============================================================================
|
|
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
|
+
*/
|
|
205
|
+
applyPendingScopes() {
|
|
206
|
+
if (!this.pendingGlobalScopes || this.scopesApplied) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const beforeOps = [];
|
|
210
|
+
const afterOps = [];
|
|
211
|
+
for (const [name, { callback, timing }] of this.pendingGlobalScopes) {
|
|
212
|
+
if (this.disabledGlobalScopes.has(name)) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const tempBuilder = new PostgresQueryBuilder(this.table, this.dataSource);
|
|
216
|
+
callback(tempBuilder);
|
|
217
|
+
if (timing === "before") {
|
|
218
|
+
beforeOps.push(...tempBuilder.operations);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
afterOps.push(...tempBuilder.operations);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
this.operations = [...beforeOps, ...this.operations, ...afterOps];
|
|
225
|
+
this.scopesApplied = true;
|
|
226
|
+
}
|
|
227
|
+
where(...args) {
|
|
228
|
+
if (args.length === 1) {
|
|
229
|
+
if (typeof args[0] === "function") {
|
|
230
|
+
// Callback for nested conditions
|
|
231
|
+
const tempBuilder = new PostgresQueryBuilder(this.table, this.dataSource);
|
|
232
|
+
args[0](tempBuilder);
|
|
233
|
+
// Wrap nested operations
|
|
234
|
+
this.addOperation("where", { nested: tempBuilder.operations });
|
|
235
|
+
}
|
|
236
|
+
else if (typeof args[0] === "object") {
|
|
237
|
+
// Object conditions
|
|
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] });
|
|
245
|
+
}
|
|
246
|
+
else if (args.length === 3) {
|
|
247
|
+
this.addOperation("where", { field: args[0], operator: args[1], value: args[2] });
|
|
248
|
+
}
|
|
249
|
+
return this;
|
|
250
|
+
}
|
|
251
|
+
orWhere(...args) {
|
|
252
|
+
if (args.length === 2) {
|
|
253
|
+
this.addOperation("orWhere", { field: args[0], operator: "=", value: args[1] });
|
|
254
|
+
}
|
|
255
|
+
else if (args.length === 3) {
|
|
256
|
+
this.addOperation("orWhere", { field: args[0], operator: args[1], value: args[2] });
|
|
257
|
+
}
|
|
258
|
+
return this;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Add a raw WHERE clause.
|
|
262
|
+
*/
|
|
263
|
+
whereRaw(expression, bindings) {
|
|
264
|
+
this.addOperation("whereRaw", { expression, bindings });
|
|
265
|
+
return this;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Add a raw OR WHERE clause.
|
|
269
|
+
*/
|
|
270
|
+
orWhereRaw(expression, bindings) {
|
|
271
|
+
this.addOperation("orWhereRaw", { expression, bindings });
|
|
272
|
+
return this;
|
|
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);
|
|
297
|
+
}
|
|
298
|
+
return this;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Check if field is between two columns.
|
|
302
|
+
*/
|
|
303
|
+
whereBetweenColumns(field, lowerColumn, upperColumn) {
|
|
304
|
+
this.addOperation("whereBetween", { field, lowerColumn, upperColumn, useColumns: true });
|
|
305
|
+
return this;
|
|
306
|
+
}
|
|
307
|
+
// ============================================================================
|
|
308
|
+
// WHERE CLAUSES - DATE OPERATIONS
|
|
309
|
+
// ============================================================================
|
|
310
|
+
/**
|
|
311
|
+
* Filter by date (ignoring time).
|
|
312
|
+
*/
|
|
313
|
+
whereDate(field, value) {
|
|
314
|
+
this.addOperation("whereDate", { field, value });
|
|
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
|
+
});
|
|
439
|
+
return this;
|
|
440
|
+
}
|
|
441
|
+
// ============================================================================
|
|
442
|
+
// WHERE CLAUSES - CONVENIENCE METHODS
|
|
443
|
+
// ============================================================================
|
|
444
|
+
/**
|
|
445
|
+
* Filter by ID.
|
|
446
|
+
*/
|
|
447
|
+
whereId(value) {
|
|
448
|
+
return this.where("id", value);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Filter by multiple IDs.
|
|
452
|
+
*/
|
|
453
|
+
whereIds(values) {
|
|
454
|
+
return this.whereIn("id", values);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Filter by UUID.
|
|
458
|
+
*/
|
|
459
|
+
whereUuid(value) {
|
|
460
|
+
return this.where("uuid", value);
|
|
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 });
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return this;
|
|
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] });
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
else {
|
|
1092
|
+
for (const [key, value] of Object.entries(input)) {
|
|
1093
|
+
this.addOperation("having", { field: key, operator: "=", value });
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
else if (args.length === 2) {
|
|
1098
|
+
this.addOperation("having", { field: args[0], operator: "=", value: args[1] });
|
|
1099
|
+
}
|
|
1100
|
+
else if (args.length === 3) {
|
|
1101
|
+
this.addOperation("having", { field: args[0], operator: args[1], value: args[2] });
|
|
1102
|
+
}
|
|
1103
|
+
return this;
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Raw HAVING clause.
|
|
1107
|
+
*/
|
|
1108
|
+
havingRaw(expression, bindings) {
|
|
1109
|
+
this.addOperation("havingRaw", { expression, bindings });
|
|
1110
|
+
return this;
|
|
1111
|
+
}
|
|
1112
|
+
// ============================================================================
|
|
1113
|
+
// EXECUTION METHODS
|
|
1114
|
+
// ============================================================================
|
|
1115
|
+
/**
|
|
1116
|
+
* Execute query and get all results.
|
|
1117
|
+
*/
|
|
1118
|
+
async get() {
|
|
1119
|
+
this.applyPendingScopes();
|
|
1120
|
+
// Apply JOIN operations for joinWith() relations
|
|
1121
|
+
this.applyJoinRelations();
|
|
1122
|
+
if (this.fetchingCallback) {
|
|
1123
|
+
await this.fetchingCallback(this);
|
|
1124
|
+
}
|
|
1125
|
+
const parser = new 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
|
+
? 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);
|
|
1246
|
+
}
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
const relationDef = this.relationDefinitions[relationName];
|
|
1250
|
+
if (!relationDef)
|
|
1251
|
+
continue;
|
|
1252
|
+
// Resolve and hydrate the related model
|
|
1253
|
+
const RelatedModel = typeof relationDef.model === "string"
|
|
1254
|
+
? getModelFromRegistry(relationDef.model)
|
|
1255
|
+
: relationDef.model;
|
|
1256
|
+
if (RelatedModel) {
|
|
1257
|
+
const relatedInstance = new RelatedModel(data);
|
|
1258
|
+
relatedInstance.isNew = false;
|
|
1259
|
+
model[relationName] = relatedInstance;
|
|
1260
|
+
if (model.loadedRelations) {
|
|
1261
|
+
model.loadedRelations.set(relationName, relatedInstance);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Get first result.
|
|
1269
|
+
*/
|
|
1270
|
+
async first() {
|
|
1271
|
+
const results = await this.limit(1).get();
|
|
1272
|
+
return results[0] ?? null;
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Get last result.
|
|
1276
|
+
*/
|
|
1277
|
+
async last() {
|
|
1278
|
+
const results = await this.orderByDesc("id").limit(1).get();
|
|
1279
|
+
return results[0] ?? null;
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Get random results.
|
|
1283
|
+
*/
|
|
1284
|
+
async random(limit) {
|
|
1285
|
+
this.orderByRaw("RANDOM()");
|
|
1286
|
+
if (limit) {
|
|
1287
|
+
this.limit(limit);
|
|
1288
|
+
}
|
|
1289
|
+
return this.get();
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Get first or throw.
|
|
1293
|
+
*/
|
|
1294
|
+
async firstOrFail() {
|
|
1295
|
+
const result = await this.first();
|
|
1296
|
+
if (!result) {
|
|
1297
|
+
throw new Error("No records found");
|
|
1298
|
+
}
|
|
1299
|
+
return result;
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Get first or call callback.
|
|
1303
|
+
*/
|
|
1304
|
+
async firstOr(callback) {
|
|
1305
|
+
const result = await this.first();
|
|
1306
|
+
return result ?? (await callback());
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Get first or return default.
|
|
1310
|
+
*/
|
|
1311
|
+
async firstOrNull() {
|
|
1312
|
+
return this.first();
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Get first or create new.
|
|
1316
|
+
*/
|
|
1317
|
+
async firstOrNew(defaults) {
|
|
1318
|
+
const result = await this.first();
|
|
1319
|
+
return result ?? defaults;
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Find by ID.
|
|
1323
|
+
*/
|
|
1324
|
+
async find(id) {
|
|
1325
|
+
return this.where("id", id).first();
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Count results.
|
|
1329
|
+
*/
|
|
1330
|
+
async count() {
|
|
1331
|
+
this.applyPendingScopes();
|
|
1332
|
+
// Build count query using selectRaw to avoid quoting COUNT(*) as a column
|
|
1333
|
+
const countOps = [
|
|
1334
|
+
...this.operations.filter((op) => op.type.includes("where") || op.type.includes("join")),
|
|
1335
|
+
{ type: "selectRaw", data: { expression: 'COUNT(*) AS "count"' } },
|
|
1336
|
+
];
|
|
1337
|
+
const parser = new PostgresQueryParser({
|
|
1338
|
+
table: this.table,
|
|
1339
|
+
operations: countOps,
|
|
1340
|
+
});
|
|
1341
|
+
const { sql, params } = parser.parse();
|
|
1342
|
+
const result = await this.driver.query(sql, params);
|
|
1343
|
+
return parseInt(result.rows[0]?.count ?? "0", 10);
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Sum of field values.
|
|
1347
|
+
*/
|
|
1348
|
+
async sum(field) {
|
|
1349
|
+
this.applyPendingScopes();
|
|
1350
|
+
const result = await this.selectRaw(`SUM(${field}) as sum`).first();
|
|
1351
|
+
return parseFloat(result?.sum ?? "0");
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Average of field values.
|
|
1355
|
+
*/
|
|
1356
|
+
async avg(field) {
|
|
1357
|
+
this.applyPendingScopes();
|
|
1358
|
+
const result = await this.selectRaw(`AVG(${field}) as avg`).first();
|
|
1359
|
+
return parseFloat(result?.avg ?? "0");
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Minimum field value.
|
|
1363
|
+
*/
|
|
1364
|
+
async min(field) {
|
|
1365
|
+
this.applyPendingScopes();
|
|
1366
|
+
const result = await this.selectRaw(`MIN(${field}) as min`).first();
|
|
1367
|
+
return parseFloat(result?.min ?? "0");
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Maximum field value.
|
|
1371
|
+
*/
|
|
1372
|
+
async max(field) {
|
|
1373
|
+
this.applyPendingScopes();
|
|
1374
|
+
const result = await this.selectRaw(`MAX(${field}) as max`).first();
|
|
1375
|
+
return parseFloat(result?.max ?? "0");
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Get distinct values.
|
|
1379
|
+
*/
|
|
1380
|
+
async distinct(field) {
|
|
1381
|
+
this.distinctValues(field);
|
|
1382
|
+
const results = await this.get();
|
|
1383
|
+
return results.map((row) => row[field]);
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Get array of values for field.
|
|
1387
|
+
*/
|
|
1388
|
+
async pluck(field) {
|
|
1389
|
+
const results = await this.select([field]).get();
|
|
1390
|
+
return results.map((row) => row[field]);
|
|
1391
|
+
}
|
|
1392
|
+
/**
|
|
1393
|
+
* Get single scalar value.
|
|
1394
|
+
*/
|
|
1395
|
+
async value(field) {
|
|
1396
|
+
const result = await this.select([field]).first();
|
|
1397
|
+
return result?.[field] ?? null;
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Check if records exist.
|
|
1401
|
+
*/
|
|
1402
|
+
async exists() {
|
|
1403
|
+
const count = await this.limit(1).count();
|
|
1404
|
+
return count > 0;
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Check if no records exist.
|
|
1408
|
+
*/
|
|
1409
|
+
async notExists() {
|
|
1410
|
+
return !(await this.exists());
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Count distinct values.
|
|
1414
|
+
*/
|
|
1415
|
+
async countDistinct(field) {
|
|
1416
|
+
const result = await this.selectRaw(`COUNT(DISTINCT ${field}) as count`).first();
|
|
1417
|
+
return parseInt(result?.count ?? "0", 10);
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Increment field value.
|
|
1421
|
+
*/
|
|
1422
|
+
async increment(field, amount = 1) {
|
|
1423
|
+
this.applyPendingScopes();
|
|
1424
|
+
const { sql, params } = this.buildFilter();
|
|
1425
|
+
const updateSql = `UPDATE ${this.driver.dialect.quoteIdentifier(this.table)} SET ${this.driver.dialect.quoteIdentifier(field)} = COALESCE(${this.driver.dialect.quoteIdentifier(field)}, 0) + $1 WHERE ${sql.replace("WHERE ", "")} RETURNING ${this.driver.dialect.quoteIdentifier(field)}`;
|
|
1426
|
+
const result = await this.driver.query(updateSql, [amount, ...params]);
|
|
1427
|
+
return result.rows[0]?.[field] ?? 0;
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Decrement field value.
|
|
1431
|
+
*/
|
|
1432
|
+
async decrement(field, amount = 1) {
|
|
1433
|
+
return this.increment(field, -amount);
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Increment for all matching.
|
|
1437
|
+
*/
|
|
1438
|
+
async incrementMany(field, amount = 1) {
|
|
1439
|
+
this.applyPendingScopes();
|
|
1440
|
+
const { sql, params } = this.buildFilter();
|
|
1441
|
+
const updateSql = `UPDATE ${this.driver.dialect.quoteIdentifier(this.table)} SET ${this.driver.dialect.quoteIdentifier(field)} = COALESCE(${this.driver.dialect.quoteIdentifier(field)}, 0) + $1 WHERE ${sql.replace("WHERE ", "")}`;
|
|
1442
|
+
const result = await this.driver.query(updateSql, [amount, ...params]);
|
|
1443
|
+
return result.rowCount ?? 0;
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Decrement for all matching.
|
|
1447
|
+
*/
|
|
1448
|
+
async decrementMany(field, amount = 1) {
|
|
1449
|
+
return this.incrementMany(field, -amount);
|
|
1450
|
+
}
|
|
1451
|
+
// ============================================================================
|
|
1452
|
+
// CHUNKING / PAGINATION
|
|
1453
|
+
// ============================================================================
|
|
1454
|
+
/**
|
|
1455
|
+
* Process results in chunks.
|
|
1456
|
+
*/
|
|
1457
|
+
async chunk(size, callback) {
|
|
1458
|
+
let chunkIndex = 0;
|
|
1459
|
+
let hasMore = true;
|
|
1460
|
+
while (hasMore) {
|
|
1461
|
+
const chunk = await this.clone()
|
|
1462
|
+
.skip(chunkIndex * size)
|
|
1463
|
+
.limit(size)
|
|
1464
|
+
.get();
|
|
1465
|
+
if (chunk.length === 0) {
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1468
|
+
const shouldContinue = await callback(chunk, chunkIndex);
|
|
1469
|
+
if (shouldContinue === false) {
|
|
1470
|
+
break;
|
|
1471
|
+
}
|
|
1472
|
+
hasMore = chunk.length === size;
|
|
1473
|
+
chunkIndex++;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Page-based pagination.
|
|
1478
|
+
*/
|
|
1479
|
+
async paginate(options) {
|
|
1480
|
+
const page = options.page ?? 1;
|
|
1481
|
+
const limit = options.limit ?? 10;
|
|
1482
|
+
const skip = (page - 1) * limit;
|
|
1483
|
+
const [data, total] = await Promise.all([
|
|
1484
|
+
this.clone().skip(skip).limit(limit).get(),
|
|
1485
|
+
this.count(),
|
|
1486
|
+
]);
|
|
1487
|
+
return {
|
|
1488
|
+
data,
|
|
1489
|
+
pagination: {
|
|
1490
|
+
total,
|
|
1491
|
+
page,
|
|
1492
|
+
limit,
|
|
1493
|
+
pages: Math.ceil(total / limit),
|
|
1494
|
+
},
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Cursor-based pagination.
|
|
1499
|
+
*/
|
|
1500
|
+
async cursorPaginate(options) {
|
|
1501
|
+
const { limit, cursor, column = "id", direction = "next" } = options;
|
|
1502
|
+
if (cursor) {
|
|
1503
|
+
const operator = direction === "next" ? ">" : "<";
|
|
1504
|
+
this.where(column, operator, cursor);
|
|
1505
|
+
}
|
|
1506
|
+
const sortOrder = direction === "next" ? "asc" : "desc";
|
|
1507
|
+
this.orderBy(column, sortOrder);
|
|
1508
|
+
const results = await this.limit(limit + 1).get();
|
|
1509
|
+
const hasMore = results.length > limit;
|
|
1510
|
+
let data = hasMore ? results.slice(0, limit) : results;
|
|
1511
|
+
if (direction === "prev") {
|
|
1512
|
+
data = data.reverse();
|
|
1513
|
+
}
|
|
1514
|
+
let nextCursor;
|
|
1515
|
+
let prevCursor;
|
|
1516
|
+
let hasPrev = false;
|
|
1517
|
+
if (data.length > 0) {
|
|
1518
|
+
const firstItem = data[0][column];
|
|
1519
|
+
const lastItem = data[data.length - 1][column];
|
|
1520
|
+
if (direction === "next") {
|
|
1521
|
+
nextCursor = hasMore ? lastItem : undefined;
|
|
1522
|
+
if (cursor) {
|
|
1523
|
+
hasPrev = true;
|
|
1524
|
+
prevCursor = firstItem;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
else {
|
|
1528
|
+
prevCursor = hasMore ? firstItem : undefined;
|
|
1529
|
+
hasPrev = hasMore;
|
|
1530
|
+
if (cursor) {
|
|
1531
|
+
nextCursor = lastItem;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
return {
|
|
1536
|
+
data,
|
|
1537
|
+
pagination: {
|
|
1538
|
+
hasMore,
|
|
1539
|
+
hasPrev,
|
|
1540
|
+
nextCursor,
|
|
1541
|
+
prevCursor,
|
|
1542
|
+
},
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
// ============================================================================
|
|
1546
|
+
// MUTATION METHODS
|
|
1547
|
+
// ============================================================================
|
|
1548
|
+
/**
|
|
1549
|
+
* Delete matching records.
|
|
1550
|
+
*/
|
|
1551
|
+
async delete() {
|
|
1552
|
+
this.applyPendingScopes();
|
|
1553
|
+
const { sql, params } = this.buildFilter();
|
|
1554
|
+
const deleteSql = `DELETE FROM ${this.driver.dialect.quoteIdentifier(this.table)} ${sql}`;
|
|
1555
|
+
const result = await this.driver.query(deleteSql, params);
|
|
1556
|
+
return result.rowCount ?? 0;
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Delete first matching record.
|
|
1560
|
+
*/
|
|
1561
|
+
async deleteOne() {
|
|
1562
|
+
return this.limit(1).delete();
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Update matching records.
|
|
1566
|
+
*/
|
|
1567
|
+
async update(fields) {
|
|
1568
|
+
this.applyPendingScopes();
|
|
1569
|
+
const result = await this.driver.updateMany(this.table, {}, { $set: fields });
|
|
1570
|
+
return result.modifiedCount;
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Unset fields from matching records.
|
|
1574
|
+
*/
|
|
1575
|
+
async unset(...fields) {
|
|
1576
|
+
this.applyPendingScopes();
|
|
1577
|
+
const updateObj = {};
|
|
1578
|
+
for (const field of fields) {
|
|
1579
|
+
updateObj[field] = 1;
|
|
1580
|
+
}
|
|
1581
|
+
const result = await this.driver.updateMany(this.table, {}, { $unset: updateObj });
|
|
1582
|
+
return result.modifiedCount;
|
|
1583
|
+
}
|
|
1584
|
+
// ============================================================================
|
|
1585
|
+
// INSPECTION / DEBUGGING
|
|
1586
|
+
// ============================================================================
|
|
1587
|
+
/**
|
|
1588
|
+
* Get the raw SQL query.
|
|
1589
|
+
*/
|
|
1590
|
+
parse() {
|
|
1591
|
+
this.applyPendingScopes();
|
|
1592
|
+
const parser = new PostgresQueryParser({
|
|
1593
|
+
table: this.table,
|
|
1594
|
+
operations: this.operations,
|
|
1595
|
+
});
|
|
1596
|
+
return parser.parse();
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Get formatted SQL string.
|
|
1600
|
+
*/
|
|
1601
|
+
pretty() {
|
|
1602
|
+
const { sql, params } = this.parse();
|
|
1603
|
+
return `${sql}\n-- Parameters: ${JSON.stringify(params)}`;
|
|
1604
|
+
}
|
|
1605
|
+
/**
|
|
1606
|
+
* Get query execution plan.
|
|
1607
|
+
*/
|
|
1608
|
+
async explain() {
|
|
1609
|
+
const { sql, params } = this.parse();
|
|
1610
|
+
const explainSql = `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${sql}`;
|
|
1611
|
+
const result = await this.driver.query(explainSql, params);
|
|
1612
|
+
return result.rows;
|
|
1613
|
+
}
|
|
1614
|
+
// ============================================================================
|
|
1615
|
+
// UTILITY METHODS
|
|
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
|
+
*/
|
|
1628
|
+
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
|
+
throw new Error(`Extension "${extension}" is not supported by PostgresQueryBuilder`);
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Tap into the query builder for side-effects.
|
|
1635
|
+
*/
|
|
1636
|
+
tap(callback) {
|
|
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;
|
|
1651
|
+
}
|
|
1652
|
+
// ============================================================================
|
|
1653
|
+
// INTERNAL HELPERS
|
|
1654
|
+
// ============================================================================
|
|
1655
|
+
/**
|
|
1656
|
+
* Build WHERE clause from current operations.
|
|
1657
|
+
*/
|
|
1658
|
+
buildFilter() {
|
|
1659
|
+
const whereOps = this.operations.filter((op) => op.type.includes("where") || op.type.includes("Where"));
|
|
1660
|
+
if (whereOps.length === 0) {
|
|
1661
|
+
return { sql: "", params: [] };
|
|
1662
|
+
}
|
|
1663
|
+
const parser = new 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
|
+
}
|
|
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
|
+
/**
|
|
1698
|
+
* Load relations using database JOINs in a single query.
|
|
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
|
|
1705
|
+
*/
|
|
1706
|
+
joinWith(...relations) {
|
|
1707
|
+
for (const relation of relations) {
|
|
1708
|
+
const def = this.relationDefinitions?.[relation];
|
|
1709
|
+
if (def) {
|
|
1710
|
+
this.joinRelations.set(relation, {
|
|
1711
|
+
alias: `_rel_${relation}`,
|
|
1712
|
+
type: def.type,
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
return this;
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* Eagerly load one or more relations.
|
|
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
|
|
1728
|
+
*/
|
|
1729
|
+
with(...args) {
|
|
1730
|
+
for (let i = 0; i < args.length; i++) {
|
|
1731
|
+
const arg = args[i];
|
|
1732
|
+
if (typeof arg === "string") {
|
|
1733
|
+
// Check if next argument is a callback for this relation
|
|
1734
|
+
const nextArg = args[i + 1];
|
|
1735
|
+
if (typeof nextArg === "function") {
|
|
1736
|
+
this.eagerLoadRelations.set(arg, nextArg);
|
|
1737
|
+
i++; // Skip the callback in next iteration
|
|
1738
|
+
}
|
|
1739
|
+
else {
|
|
1740
|
+
this.eagerLoadRelations.set(arg, true);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
else if (typeof arg === "object" && arg !== null) {
|
|
1744
|
+
for (const [key, value] of Object.entries(arg)) {
|
|
1745
|
+
this.eagerLoadRelations.set(key, value);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
// Functions not preceded by a string are ignored (invalid usage)
|
|
1749
|
+
}
|
|
1750
|
+
return this;
|
|
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;
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Filter results that don't have any related models.
|
|
1783
|
+
* @param relation - Relation name
|
|
1784
|
+
*/
|
|
1785
|
+
doesntHave(relation) {
|
|
1786
|
+
// TODO: Implement doesntHave() using NOT EXISTS subquery
|
|
1787
|
+
this.addOperation("doesntHave", { relation });
|
|
1788
|
+
return this;
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Filter results that don't have related models matching specific conditions.
|
|
1792
|
+
* @param relation - Relation name
|
|
1793
|
+
* @param callback - Callback to define conditions
|
|
1794
|
+
*/
|
|
1795
|
+
whereDoesntHave(relation, callback) {
|
|
1796
|
+
// TODO: Implement whereDoesntHave() using NOT EXISTS subquery with conditions
|
|
1797
|
+
this.addOperation("whereDoesntHave", { relation, callback });
|
|
1798
|
+
return this;
|
|
1799
|
+
}
|
|
1800
|
+
}export{PostgresQueryBuilder};//# sourceMappingURL=postgres-query-builder.js.map
|