relq 1.0.0
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/LICENSE +21 -0
- package/README.md +862 -0
- package/dist/addons/buffer.js +1869 -0
- package/dist/addons/pg-cursor.js +1425 -0
- package/dist/addons/pg-format.js +2248 -0
- package/dist/addons/pg.js +4790 -0
- package/dist/bin/relq.js +2 -0
- package/dist/cjs/cache/index.cjs +9 -0
- package/dist/cjs/cache/query-cache.cjs +311 -0
- package/dist/cjs/cli/commands/add.cjs +82 -0
- package/dist/cjs/cli/commands/commit.cjs +145 -0
- package/dist/cjs/cli/commands/diff.cjs +84 -0
- package/dist/cjs/cli/commands/export.cjs +333 -0
- package/dist/cjs/cli/commands/fetch.cjs +59 -0
- package/dist/cjs/cli/commands/generate.cjs +242 -0
- package/dist/cjs/cli/commands/history.cjs +165 -0
- package/dist/cjs/cli/commands/import.cjs +524 -0
- package/dist/cjs/cli/commands/init.cjs +437 -0
- package/dist/cjs/cli/commands/introspect.cjs +142 -0
- package/dist/cjs/cli/commands/log.cjs +62 -0
- package/dist/cjs/cli/commands/migrate.cjs +167 -0
- package/dist/cjs/cli/commands/pull.cjs +410 -0
- package/dist/cjs/cli/commands/push.cjs +165 -0
- package/dist/cjs/cli/commands/rollback.cjs +169 -0
- package/dist/cjs/cli/commands/status.cjs +110 -0
- package/dist/cjs/cli/commands/sync.cjs +79 -0
- package/dist/cjs/cli/index.cjs +275 -0
- package/dist/cjs/cli/utils/change-tracker.cjs +446 -0
- package/dist/cjs/cli/utils/commit-manager.cjs +239 -0
- package/dist/cjs/cli/utils/config-loader.cjs +127 -0
- package/dist/cjs/cli/utils/env-loader.cjs +62 -0
- package/dist/cjs/cli/utils/fast-introspect.cjs +398 -0
- package/dist/cjs/cli/utils/git-utils.cjs +404 -0
- package/dist/cjs/cli/utils/migration-generator.cjs +269 -0
- package/dist/cjs/cli/utils/relqignore.cjs +114 -0
- package/dist/cjs/cli/utils/repo-manager.cjs +515 -0
- package/dist/cjs/cli/utils/schema-comparator.cjs +313 -0
- package/dist/cjs/cli/utils/schema-diff.cjs +284 -0
- package/dist/cjs/cli/utils/schema-hash.cjs +108 -0
- package/dist/cjs/cli/utils/schema-introspect.cjs +455 -0
- package/dist/cjs/cli/utils/snapshot-manager.cjs +223 -0
- package/dist/cjs/cli/utils/spinner.cjs +108 -0
- package/dist/cjs/cli/utils/sql-generator.cjs +520 -0
- package/dist/cjs/cli/utils/sql-parser.cjs +999 -0
- package/dist/cjs/cli/utils/type-generator.cjs +2061 -0
- package/dist/cjs/condition/array-condition-builder.cjs +503 -0
- package/dist/cjs/condition/array-numeric-condition-builder.cjs +186 -0
- package/dist/cjs/condition/array-specialized-condition-builder.cjs +206 -0
- package/dist/cjs/condition/array-string-condition-builder.cjs +146 -0
- package/dist/cjs/condition/base-condition-builder.cjs +2 -0
- package/dist/cjs/condition/condition-collector.cjs +284 -0
- package/dist/cjs/condition/fulltext-condition-builder.cjs +61 -0
- package/dist/cjs/condition/geometric-condition-builder.cjs +208 -0
- package/dist/cjs/condition/index.cjs +25 -0
- package/dist/cjs/condition/jsonb-condition-builder.cjs +160 -0
- package/dist/cjs/condition/network-condition-builder.cjs +230 -0
- package/dist/cjs/condition/range-condition-builder.cjs +82 -0
- package/dist/cjs/config/config.cjs +190 -0
- package/dist/cjs/config/index.cjs +9 -0
- package/dist/cjs/constants/pg-values.cjs +68 -0
- package/dist/cjs/copy/copy-builder.cjs +316 -0
- package/dist/cjs/copy/index.cjs +6 -0
- package/dist/cjs/core/query-builder.cjs +440 -0
- package/dist/cjs/core/relq-client.cjs +1831 -0
- package/dist/cjs/core/typed-kuery-client.cjs +2 -0
- package/dist/cjs/count/count-builder.cjs +88 -0
- package/dist/cjs/count/index.cjs +5 -0
- package/dist/cjs/cte/cte-builder.cjs +89 -0
- package/dist/cjs/cte/index.cjs +5 -0
- package/dist/cjs/ddl/function.cjs +48 -0
- package/dist/cjs/ddl/index.cjs +7 -0
- package/dist/cjs/ddl/sql.cjs +54 -0
- package/dist/cjs/delete/delete-builder.cjs +135 -0
- package/dist/cjs/delete/index.cjs +5 -0
- package/dist/cjs/errors/relq-errors.cjs +329 -0
- package/dist/cjs/examples/fulltext-search-test.cjs +122 -0
- package/dist/cjs/explain/explain-builder.cjs +99 -0
- package/dist/cjs/explain/index.cjs +5 -0
- package/dist/cjs/function/create-function-builder.cjs +196 -0
- package/dist/cjs/function/index.cjs +6 -0
- package/dist/cjs/functions/advanced-functions.cjs +241 -0
- package/dist/cjs/functions/case-builder.cjs +66 -0
- package/dist/cjs/functions/geometric-functions.cjs +104 -0
- package/dist/cjs/functions/index.cjs +184 -0
- package/dist/cjs/functions/network-functions.cjs +86 -0
- package/dist/cjs/functions/sql-functions.cjs +431 -0
- package/dist/cjs/index.cjs +164 -0
- package/dist/cjs/indexing/create-index-builder.cjs +187 -0
- package/dist/cjs/indexing/drop-index-builder.cjs +89 -0
- package/dist/cjs/indexing/index-types.cjs +2 -0
- package/dist/cjs/indexing/index.cjs +8 -0
- package/dist/cjs/insert/conflict-builder.cjs +173 -0
- package/dist/cjs/insert/index.cjs +5 -0
- package/dist/cjs/insert/insert-builder.cjs +254 -0
- package/dist/cjs/introspect/index.cjs +229 -0
- package/dist/cjs/maintenance/index.cjs +6 -0
- package/dist/cjs/maintenance/vacuum-builder.cjs +166 -0
- package/dist/cjs/pubsub/index.cjs +7 -0
- package/dist/cjs/pubsub/listen-notify-builder.cjs +57 -0
- package/dist/cjs/pubsub/listener-connection.cjs +180 -0
- package/dist/cjs/raw/index.cjs +5 -0
- package/dist/cjs/raw/raw-query-builder.cjs +27 -0
- package/dist/cjs/schema/index.cjs +15 -0
- package/dist/cjs/schema/schema-builder.cjs +1167 -0
- package/dist/cjs/schema-builder.cjs +21 -0
- package/dist/cjs/schema-definition/column-types.cjs +829 -0
- package/dist/cjs/schema-definition/index.cjs +62 -0
- package/dist/cjs/schema-definition/introspection.cjs +620 -0
- package/dist/cjs/schema-definition/partitions.cjs +129 -0
- package/dist/cjs/schema-definition/pg-enum.cjs +76 -0
- package/dist/cjs/schema-definition/pg-function.cjs +91 -0
- package/dist/cjs/schema-definition/pg-sequence.cjs +56 -0
- package/dist/cjs/schema-definition/pg-trigger.cjs +108 -0
- package/dist/cjs/schema-definition/relations.cjs +98 -0
- package/dist/cjs/schema-definition/sql-expressions.cjs +202 -0
- package/dist/cjs/schema-definition/table-definition.cjs +636 -0
- package/dist/cjs/select/aggregate-builder.cjs +179 -0
- package/dist/cjs/select/index.cjs +5 -0
- package/dist/cjs/select/select-builder.cjs +233 -0
- package/dist/cjs/sequence/index.cjs +7 -0
- package/dist/cjs/sequence/sequence-builder.cjs +264 -0
- package/dist/cjs/table/alter-table-builder.cjs +146 -0
- package/dist/cjs/table/constraint-builder.cjs +102 -0
- package/dist/cjs/table/create-table-builder.cjs +248 -0
- package/dist/cjs/table/index.cjs +17 -0
- package/dist/cjs/table/partition-builder.cjs +131 -0
- package/dist/cjs/table/truncate-builder.cjs +70 -0
- package/dist/cjs/transaction/index.cjs +6 -0
- package/dist/cjs/transaction/transaction-builder.cjs +78 -0
- package/dist/cjs/trigger/create-trigger-builder.cjs +174 -0
- package/dist/cjs/trigger/index.cjs +6 -0
- package/dist/cjs/types/aggregate-types.cjs +2 -0
- package/dist/cjs/types/config-types.cjs +40 -0
- package/dist/cjs/types/inference-types.cjs +18 -0
- package/dist/cjs/types/pagination-types.cjs +7 -0
- package/dist/cjs/types/result-types.cjs +2 -0
- package/dist/cjs/types/schema-types.cjs +2 -0
- package/dist/cjs/types/subscription-types.cjs +2 -0
- package/dist/cjs/types.cjs +2 -0
- package/dist/cjs/update/array-update-builder.cjs +205 -0
- package/dist/cjs/update/index.cjs +13 -0
- package/dist/cjs/update/update-builder.cjs +195 -0
- package/dist/cjs/utils/case-converter.cjs +58 -0
- package/dist/cjs/utils/environment-detection.cjs +120 -0
- package/dist/cjs/utils/index.cjs +10 -0
- package/dist/cjs/utils/pool-defaults.cjs +106 -0
- package/dist/cjs/utils/type-coercion.cjs +118 -0
- package/dist/cjs/view/create-view-builder.cjs +180 -0
- package/dist/cjs/view/index.cjs +7 -0
- package/dist/cjs/window/index.cjs +5 -0
- package/dist/cjs/window/window-builder.cjs +80 -0
- package/dist/config.cjs +1 -0
- package/dist/config.d.ts +655 -0
- package/dist/config.js +1 -0
- package/dist/esm/cache/index.js +1 -0
- package/dist/esm/cache/query-cache.js +303 -0
- package/dist/esm/cli/commands/add.js +78 -0
- package/dist/esm/cli/commands/commit.js +109 -0
- package/dist/esm/cli/commands/diff.js +81 -0
- package/dist/esm/cli/commands/export.js +297 -0
- package/dist/esm/cli/commands/fetch.js +56 -0
- package/dist/esm/cli/commands/generate.js +206 -0
- package/dist/esm/cli/commands/history.js +129 -0
- package/dist/esm/cli/commands/import.js +488 -0
- package/dist/esm/cli/commands/init.js +401 -0
- package/dist/esm/cli/commands/introspect.js +106 -0
- package/dist/esm/cli/commands/log.js +59 -0
- package/dist/esm/cli/commands/migrate.js +131 -0
- package/dist/esm/cli/commands/pull.js +374 -0
- package/dist/esm/cli/commands/push.js +129 -0
- package/dist/esm/cli/commands/rollback.js +133 -0
- package/dist/esm/cli/commands/status.js +107 -0
- package/dist/esm/cli/commands/sync.js +76 -0
- package/dist/esm/cli/index.js +240 -0
- package/dist/esm/cli/utils/change-tracker.js +405 -0
- package/dist/esm/cli/utils/commit-manager.js +191 -0
- package/dist/esm/cli/utils/config-loader.js +86 -0
- package/dist/esm/cli/utils/env-loader.js +57 -0
- package/dist/esm/cli/utils/fast-introspect.js +362 -0
- package/dist/esm/cli/utils/git-utils.js +347 -0
- package/dist/esm/cli/utils/migration-generator.js +263 -0
- package/dist/esm/cli/utils/relqignore.js +74 -0
- package/dist/esm/cli/utils/repo-manager.js +444 -0
- package/dist/esm/cli/utils/schema-comparator.js +307 -0
- package/dist/esm/cli/utils/schema-diff.js +276 -0
- package/dist/esm/cli/utils/schema-hash.js +69 -0
- package/dist/esm/cli/utils/schema-introspect.js +418 -0
- package/dist/esm/cli/utils/snapshot-manager.js +179 -0
- package/dist/esm/cli/utils/spinner.js +101 -0
- package/dist/esm/cli/utils/sql-generator.js +504 -0
- package/dist/esm/cli/utils/sql-parser.js +992 -0
- package/dist/esm/cli/utils/type-generator.js +2058 -0
- package/dist/esm/condition/array-condition-builder.js +495 -0
- package/dist/esm/condition/array-numeric-condition-builder.js +182 -0
- package/dist/esm/condition/array-specialized-condition-builder.js +200 -0
- package/dist/esm/condition/array-string-condition-builder.js +142 -0
- package/dist/esm/condition/base-condition-builder.js +1 -0
- package/dist/esm/condition/condition-collector.js +275 -0
- package/dist/esm/condition/fulltext-condition-builder.js +53 -0
- package/dist/esm/condition/geometric-condition-builder.js +200 -0
- package/dist/esm/condition/index.js +7 -0
- package/dist/esm/condition/jsonb-condition-builder.js +152 -0
- package/dist/esm/condition/network-condition-builder.js +222 -0
- package/dist/esm/condition/range-condition-builder.js +74 -0
- package/dist/esm/config/config.js +150 -0
- package/dist/esm/config/index.js +1 -0
- package/dist/esm/constants/pg-values.js +63 -0
- package/dist/esm/copy/copy-builder.js +308 -0
- package/dist/esm/copy/index.js +1 -0
- package/dist/esm/core/query-builder.js +426 -0
- package/dist/esm/core/relq-client.js +1791 -0
- package/dist/esm/core/typed-kuery-client.js +1 -0
- package/dist/esm/count/count-builder.js +81 -0
- package/dist/esm/count/index.js +1 -0
- package/dist/esm/cte/cte-builder.js +82 -0
- package/dist/esm/cte/index.js +1 -0
- package/dist/esm/ddl/function.js +45 -0
- package/dist/esm/ddl/index.js +2 -0
- package/dist/esm/ddl/sql.js +51 -0
- package/dist/esm/delete/delete-builder.js +128 -0
- package/dist/esm/delete/index.js +1 -0
- package/dist/esm/errors/relq-errors.js +310 -0
- package/dist/esm/examples/fulltext-search-test.js +117 -0
- package/dist/esm/explain/explain-builder.js +95 -0
- package/dist/esm/explain/index.js +1 -0
- package/dist/esm/function/create-function-builder.js +188 -0
- package/dist/esm/function/index.js +1 -0
- package/dist/esm/functions/advanced-functions.js +231 -0
- package/dist/esm/functions/case-builder.js +58 -0
- package/dist/esm/functions/geometric-functions.js +97 -0
- package/dist/esm/functions/index.js +171 -0
- package/dist/esm/functions/network-functions.js +79 -0
- package/dist/esm/functions/sql-functions.js +421 -0
- package/dist/esm/index.js +34 -0
- package/dist/esm/indexing/create-index-builder.js +180 -0
- package/dist/esm/indexing/drop-index-builder.js +81 -0
- package/dist/esm/indexing/index-types.js +1 -0
- package/dist/esm/indexing/index.js +2 -0
- package/dist/esm/insert/conflict-builder.js +162 -0
- package/dist/esm/insert/index.js +1 -0
- package/dist/esm/insert/insert-builder.js +247 -0
- package/dist/esm/introspect/index.js +224 -0
- package/dist/esm/maintenance/index.js +1 -0
- package/dist/esm/maintenance/vacuum-builder.js +158 -0
- package/dist/esm/pubsub/index.js +1 -0
- package/dist/esm/pubsub/listen-notify-builder.js +48 -0
- package/dist/esm/pubsub/listener-connection.js +173 -0
- package/dist/esm/raw/index.js +1 -0
- package/dist/esm/raw/raw-query-builder.js +20 -0
- package/dist/esm/schema/index.js +1 -0
- package/dist/esm/schema/schema-builder.js +1150 -0
- package/dist/esm/schema-builder.js +2 -0
- package/dist/esm/schema-definition/column-types.js +738 -0
- package/dist/esm/schema-definition/index.js +10 -0
- package/dist/esm/schema-definition/introspection.js +614 -0
- package/dist/esm/schema-definition/partitions.js +123 -0
- package/dist/esm/schema-definition/pg-enum.js +70 -0
- package/dist/esm/schema-definition/pg-function.js +85 -0
- package/dist/esm/schema-definition/pg-sequence.js +50 -0
- package/dist/esm/schema-definition/pg-trigger.js +102 -0
- package/dist/esm/schema-definition/relations.js +90 -0
- package/dist/esm/schema-definition/sql-expressions.js +193 -0
- package/dist/esm/schema-definition/table-definition.js +630 -0
- package/dist/esm/select/aggregate-builder.js +172 -0
- package/dist/esm/select/index.js +1 -0
- package/dist/esm/select/select-builder.js +226 -0
- package/dist/esm/sequence/index.js +1 -0
- package/dist/esm/sequence/sequence-builder.js +255 -0
- package/dist/esm/table/alter-table-builder.js +138 -0
- package/dist/esm/table/constraint-builder.js +95 -0
- package/dist/esm/table/create-table-builder.js +241 -0
- package/dist/esm/table/index.js +5 -0
- package/dist/esm/table/partition-builder.js +121 -0
- package/dist/esm/table/truncate-builder.js +63 -0
- package/dist/esm/transaction/index.js +1 -0
- package/dist/esm/transaction/transaction-builder.js +70 -0
- package/dist/esm/trigger/create-trigger-builder.js +166 -0
- package/dist/esm/trigger/index.js +1 -0
- package/dist/esm/types/aggregate-types.js +1 -0
- package/dist/esm/types/config-types.js +36 -0
- package/dist/esm/types/inference-types.js +12 -0
- package/dist/esm/types/pagination-types.js +4 -0
- package/dist/esm/types/result-types.js +1 -0
- package/dist/esm/types/schema-types.js +1 -0
- package/dist/esm/types/subscription-types.js +1 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/update/array-update-builder.js +192 -0
- package/dist/esm/update/index.js +2 -0
- package/dist/esm/update/update-builder.js +188 -0
- package/dist/esm/utils/case-converter.js +55 -0
- package/dist/esm/utils/environment-detection.js +113 -0
- package/dist/esm/utils/index.js +2 -0
- package/dist/esm/utils/pool-defaults.js +100 -0
- package/dist/esm/utils/type-coercion.js +110 -0
- package/dist/esm/view/create-view-builder.js +171 -0
- package/dist/esm/view/index.js +1 -0
- package/dist/esm/window/index.js +1 -0
- package/dist/esm/window/window-builder.js +73 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +10341 -0
- package/dist/index.js +1 -0
- package/dist/schema-builder.cjs +1 -0
- package/dist/schema-builder.d.ts +2272 -0
- package/dist/schema-builder.js +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,1791 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { toPoolConfig } from "../types/config-types.js";
|
|
3
|
+
import { SelectBuilder } from "../select/select-builder.js";
|
|
4
|
+
import { AggregateQueryBuilder } from "../select/aggregate-builder.js";
|
|
5
|
+
import { InsertBuilder } from "../insert/insert-builder.js";
|
|
6
|
+
import { UpdateBuilder } from "../update/update-builder.js";
|
|
7
|
+
import { DeleteBuilder } from "../delete/delete-builder.js";
|
|
8
|
+
import { CountBuilder } from "../count/count-builder.js";
|
|
9
|
+
import { RawQueryBuilder } from "../raw/raw-query-builder.js";
|
|
10
|
+
import { TransactionBuilder } from "../transaction/transaction-builder.js";
|
|
11
|
+
import { ListenerConnection } from "../pubsub/listener-connection.js";
|
|
12
|
+
import { detectEnvironment, getEnvironmentDescription } from "../utils/environment-detection.js";
|
|
13
|
+
import { validatePoolConfig, formatPoolConfig, getSmartPoolDefaults } from "../utils/pool-defaults.js";
|
|
14
|
+
import { RelqEnvironmentError, RelqConfigError, RelqQueryError, RelqConnectionError, parsePostgresError } from "../errors/relq-errors.js";
|
|
15
|
+
import { randomLimit } from "../types/pagination-types.js";
|
|
16
|
+
import { deserializeValue, serializeValue } from "../utils/type-coercion.js";
|
|
17
|
+
import Cursor from "../../addons/pg-cursor.js";
|
|
18
|
+
const INTERNAL = Symbol('relq-internal');
|
|
19
|
+
let PgPool = null;
|
|
20
|
+
let PgClient = null;
|
|
21
|
+
const activeRelqInstances = new Set();
|
|
22
|
+
let cleanupHandlersRegistered = false;
|
|
23
|
+
function debugLog(config, ...args) {
|
|
24
|
+
if (config?.logLevel === 'debug') {
|
|
25
|
+
console.log('[Relq DEBUG]', ...args);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function registerGlobalCleanupHandlers() {
|
|
29
|
+
if (cleanupHandlersRegistered)
|
|
30
|
+
return;
|
|
31
|
+
if (typeof process === 'undefined' || !process.on)
|
|
32
|
+
return;
|
|
33
|
+
process.on('beforeExit', async () => {
|
|
34
|
+
if (activeRelqInstances.size === 0)
|
|
35
|
+
return;
|
|
36
|
+
await Promise.all(Array.from(activeRelqInstances).map(instance => instance.close().catch(err => console.error('Error closing database connection:', err))));
|
|
37
|
+
});
|
|
38
|
+
cleanupHandlersRegistered = true;
|
|
39
|
+
}
|
|
40
|
+
async function loadPg() {
|
|
41
|
+
if (!PgPool || !PgClient) {
|
|
42
|
+
try {
|
|
43
|
+
const pg = await import("../addon/pg.js");
|
|
44
|
+
PgPool = pg.Pool;
|
|
45
|
+
PgClient = pg.Client;
|
|
46
|
+
return { Pool: pg.Pool, Client: pg.Client };
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
throw new RelqConfigError('Failed to load "pg" module', { field: 'pg', value: error instanceof Error ? error.message : String(error) });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { Pool: PgPool, Client: PgClient };
|
|
53
|
+
}
|
|
54
|
+
export class Relq {
|
|
55
|
+
pool;
|
|
56
|
+
client;
|
|
57
|
+
config;
|
|
58
|
+
usePooling;
|
|
59
|
+
initialized = false;
|
|
60
|
+
initPromise;
|
|
61
|
+
isClosed = false;
|
|
62
|
+
environment;
|
|
63
|
+
poolErrorHandler;
|
|
64
|
+
poolConnectHandler;
|
|
65
|
+
poolRemoveHandler;
|
|
66
|
+
listener = null;
|
|
67
|
+
emitter = new EventEmitter();
|
|
68
|
+
columnMappings = new Map();
|
|
69
|
+
schema;
|
|
70
|
+
constructor(schema, config) {
|
|
71
|
+
const schemaObj = schema;
|
|
72
|
+
this.config = config;
|
|
73
|
+
this.schema = schema;
|
|
74
|
+
this.environment = detectEnvironment();
|
|
75
|
+
if (schema) {
|
|
76
|
+
this.buildColumnMappings(schema);
|
|
77
|
+
}
|
|
78
|
+
this.usePooling = this.determinePoolingStrategy(config);
|
|
79
|
+
this.validateConfiguration();
|
|
80
|
+
this.logEnvironmentInfo();
|
|
81
|
+
activeRelqInstances.add(this);
|
|
82
|
+
registerGlobalCleanupHandlers();
|
|
83
|
+
}
|
|
84
|
+
buildColumnMappings(schema) {
|
|
85
|
+
if (!schema || typeof schema !== 'object')
|
|
86
|
+
return;
|
|
87
|
+
const tables = schema.tables || schema;
|
|
88
|
+
for (const [tableName, tableDef] of Object.entries(tables)) {
|
|
89
|
+
if (!tableDef || typeof tableDef !== 'object')
|
|
90
|
+
continue;
|
|
91
|
+
const columns = tableDef.$columns;
|
|
92
|
+
if (!columns || typeof columns !== 'object')
|
|
93
|
+
continue;
|
|
94
|
+
const propToDb = new Map();
|
|
95
|
+
const dbToProp = new Map();
|
|
96
|
+
const propToType = new Map();
|
|
97
|
+
const propToCheckValues = new Map();
|
|
98
|
+
const propToValidate = new Map();
|
|
99
|
+
const propToFields = new Map();
|
|
100
|
+
for (const [propName, colDef] of Object.entries(columns)) {
|
|
101
|
+
const dbColName = colDef?.$columnName ?? propName;
|
|
102
|
+
const colType = colDef?.$type ?? 'TEXT';
|
|
103
|
+
propToDb.set(propName, dbColName);
|
|
104
|
+
dbToProp.set(dbColName, propName);
|
|
105
|
+
propToType.set(propName, colType);
|
|
106
|
+
debugLog(this.config, `buildColumnMappings: ${tableName}.${propName} -> type=${colType}, $validate=${!!colDef?.$validate}, $fields=${!!colDef?.$fields}, $checkValues=${!!colDef?.$checkValues}`);
|
|
107
|
+
if (colDef?.$checkValues && Array.isArray(colDef.$checkValues)) {
|
|
108
|
+
propToCheckValues.set(propName, colDef.$checkValues);
|
|
109
|
+
}
|
|
110
|
+
if (colDef?.$validate && typeof colDef.$validate === 'function') {
|
|
111
|
+
propToValidate.set(propName, colDef.$validate);
|
|
112
|
+
}
|
|
113
|
+
if (colDef?.$fields && typeof colDef.$fields === 'object') {
|
|
114
|
+
propToFields.set(propName, colDef.$fields);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const dbTableName = tableDef.$name ?? tableName;
|
|
118
|
+
this.columnMappings.set(dbTableName, {
|
|
119
|
+
propToDb,
|
|
120
|
+
dbToProp,
|
|
121
|
+
propToType,
|
|
122
|
+
propToCheckValues,
|
|
123
|
+
propToValidate,
|
|
124
|
+
propToFields
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
_transformToDbColumns(tableName, data) {
|
|
129
|
+
const mapping = this.columnMappings.get(tableName);
|
|
130
|
+
if (!mapping)
|
|
131
|
+
return data;
|
|
132
|
+
const result = {};
|
|
133
|
+
for (const [key, value] of Object.entries(data)) {
|
|
134
|
+
const dbColName = mapping.propToDb.get(key) ?? key;
|
|
135
|
+
const colType = mapping.propToType.get(key);
|
|
136
|
+
if (colType && typeof colType === 'string') {
|
|
137
|
+
result[dbColName] = serializeValue(value, colType);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
result[dbColName] = value;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
_transformFromDbColumns(tableName, data) {
|
|
146
|
+
const mapping = this.columnMappings.get(tableName);
|
|
147
|
+
if (!mapping)
|
|
148
|
+
return data;
|
|
149
|
+
const result = {};
|
|
150
|
+
for (const [key, value] of Object.entries(data)) {
|
|
151
|
+
const propName = mapping.dbToProp.get(key) ?? key;
|
|
152
|
+
const colType = mapping.propToType.get(propName);
|
|
153
|
+
if (colType && typeof colType === 'string') {
|
|
154
|
+
result[propName] = deserializeValue(value, colType);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
result[propName] = value;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
_transformResultsFromDb(tableName, rows) {
|
|
163
|
+
const mapping = this.columnMappings.get(tableName);
|
|
164
|
+
if (!mapping)
|
|
165
|
+
return rows;
|
|
166
|
+
return rows.map(row => this._transformFromDbColumns(tableName, row));
|
|
167
|
+
}
|
|
168
|
+
_hasColumnMapping() {
|
|
169
|
+
return this.columnMappings.size > 0;
|
|
170
|
+
}
|
|
171
|
+
_validateData(tableName, data, operation) {
|
|
172
|
+
const validation = this.config.validation;
|
|
173
|
+
if (validation?.enabled === false)
|
|
174
|
+
return;
|
|
175
|
+
const mapping = this.columnMappings.get(tableName);
|
|
176
|
+
if (!mapping) {
|
|
177
|
+
debugLog(this.config, `No mapping found for table: ${tableName}. Available:`, Array.from(this.columnMappings.keys()));
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const errors = [];
|
|
181
|
+
const validateLength = validation?.validateLength !== false;
|
|
182
|
+
const validateTypes = validation?.validateTypes !== false;
|
|
183
|
+
const onError = validation?.onError ?? 'throw';
|
|
184
|
+
debugLog(this.config, `Validating ${operation} on ${tableName}. Fields:`, Object.keys(data));
|
|
185
|
+
for (const [propName, value] of Object.entries(data)) {
|
|
186
|
+
if (value === null || value === undefined)
|
|
187
|
+
continue;
|
|
188
|
+
const colType = mapping.propToType.get(propName);
|
|
189
|
+
const colTypeStr = typeof colType === 'string' ? colType : null;
|
|
190
|
+
if (validateTypes && colTypeStr) {
|
|
191
|
+
const jsType = typeof value;
|
|
192
|
+
const upperType = colTypeStr.toUpperCase();
|
|
193
|
+
if (['INTEGER', 'INT', 'SMALLINT', 'BIGINT', 'INT2', 'INT4', 'INT8', 'SERIAL'].some(t => upperType.includes(t))) {
|
|
194
|
+
if (jsType !== 'number' && jsType !== 'bigint' && jsType !== 'string') {
|
|
195
|
+
errors.push(`${propName}: expected number for ${colType}, got ${jsType}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else if (['REAL', 'DOUBLE', 'FLOAT', 'NUMERIC', 'DECIMAL'].some(t => upperType.includes(t))) {
|
|
199
|
+
if (jsType !== 'number' && jsType !== 'string') {
|
|
200
|
+
errors.push(`${propName}: expected number for ${colType}, got ${jsType}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else if (upperType.includes('BOOL')) {
|
|
204
|
+
if (jsType !== 'boolean') {
|
|
205
|
+
errors.push(`${propName}: expected boolean for ${colType}, got ${jsType}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else if (['VARCHAR', 'CHAR', 'TEXT', 'UUID'].some(t => upperType.includes(t))) {
|
|
209
|
+
if (jsType !== 'string') {
|
|
210
|
+
errors.push(`${propName}: expected string for ${colType}, got ${jsType}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (validateLength && typeof value === 'string' && colTypeStr) {
|
|
215
|
+
const lengthMatch = colTypeStr.match(/(?:var)?char\((\d+)\)/i);
|
|
216
|
+
if (lengthMatch) {
|
|
217
|
+
const maxLength = parseInt(lengthMatch[1], 10);
|
|
218
|
+
if (value.length > maxLength) {
|
|
219
|
+
errors.push(`${propName}: string length ${value.length} exceeds ${colTypeStr} limit of ${maxLength}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const validateFn = mapping.propToValidate.get(propName);
|
|
224
|
+
if (validateFn) {
|
|
225
|
+
const isValid = validateFn(value);
|
|
226
|
+
debugLog(this.config, `Domain validation for '${propName}' with value '${value}': isValid=${isValid}`);
|
|
227
|
+
if (!isValid) {
|
|
228
|
+
errors.push(`${propName}: value '${value}' failed domain validation`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const compositeFields = mapping.propToFields.get(propName);
|
|
232
|
+
if (compositeFields && value && typeof value === 'object') {
|
|
233
|
+
this._validateComposite(propName, value, compositeFields, errors);
|
|
234
|
+
}
|
|
235
|
+
const checkValues = mapping.propToCheckValues.get(propName);
|
|
236
|
+
if (checkValues && checkValues.length > 0 && typeof value === 'string') {
|
|
237
|
+
if (!checkValues.includes(value)) {
|
|
238
|
+
errors.push(`${propName}: value '${value}' not in allowed values [${checkValues.join(', ')}]`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (errors.length > 0) {
|
|
243
|
+
const message = `Validation failed for ${operation} on ${tableName}:\n - ${errors.join('\n - ')}`;
|
|
244
|
+
debugLog(this.config, message);
|
|
245
|
+
if (onError === 'throw') {
|
|
246
|
+
throw new RelqQueryError(message);
|
|
247
|
+
}
|
|
248
|
+
else if (onError === 'warn') {
|
|
249
|
+
console.warn('[Relq]', message);
|
|
250
|
+
}
|
|
251
|
+
else if (onError === 'log') {
|
|
252
|
+
console.log('[Relq]', message);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
_validateComposite(propName, value, fields, errors) {
|
|
257
|
+
const validation = this.config.validation;
|
|
258
|
+
const validateLength = validation?.validateLength !== false;
|
|
259
|
+
const validateTypes = validation?.validateTypes !== false;
|
|
260
|
+
for (const [fieldName, colDef] of Object.entries(fields)) {
|
|
261
|
+
const colConfig = colDef;
|
|
262
|
+
const fieldValue = value[fieldName];
|
|
263
|
+
const colType = colConfig.$sqlType ?? colConfig.$config?.$type ?? (typeof colConfig.$type === 'string' ? colConfig.$type : 'TEXT');
|
|
264
|
+
const upperType = colType.toUpperCase();
|
|
265
|
+
const isNullable = colConfig.$config?.$nullable ?? colConfig.$nullable;
|
|
266
|
+
debugLog(this.config, `Composite field '${propName}.${fieldName}': value=${JSON.stringify(fieldValue)}, colType=${colType}, nullable=${isNullable}`);
|
|
267
|
+
if (fieldValue === null || fieldValue === undefined) {
|
|
268
|
+
if (isNullable === false) {
|
|
269
|
+
errors.push(`${propName}.${fieldName}: cannot be null`);
|
|
270
|
+
}
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (validateTypes && upperType) {
|
|
274
|
+
const jsType = typeof fieldValue;
|
|
275
|
+
if (['INTEGER', 'INT', 'SMALLINT', 'BIGINT', 'SERIAL'].some(t => upperType.includes(t))) {
|
|
276
|
+
if (jsType !== 'number' && jsType !== 'bigint' && jsType !== 'string') {
|
|
277
|
+
errors.push(`${propName}.${fieldName}: expected number, got ${jsType}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else if (['VARCHAR', 'CHAR', 'TEXT'].some(t => upperType.includes(t))) {
|
|
281
|
+
if (jsType !== 'string') {
|
|
282
|
+
errors.push(`${propName}.${fieldName}: expected string, got ${jsType}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (validateLength && typeof fieldValue === 'string' && typeof colType === 'string') {
|
|
287
|
+
const lengthMatch = colType.match(/(?:var)?char\((\d+)\)/i);
|
|
288
|
+
if (lengthMatch) {
|
|
289
|
+
const maxLength = parseInt(lengthMatch[1], 10);
|
|
290
|
+
if (fieldValue.length > maxLength) {
|
|
291
|
+
errors.push(`${propName}.${fieldName}: length ${fieldValue.length} exceeds ${maxLength}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (colConfig.$validate && typeof colConfig.$validate === 'function') {
|
|
296
|
+
if (!colConfig.$validate(fieldValue)) {
|
|
297
|
+
errors.push(`${propName}.${fieldName}: failed domain validation`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (colConfig.$fields && fieldValue && typeof fieldValue === 'object') {
|
|
301
|
+
this._validateComposite(`${propName}.${fieldName}`, fieldValue, colConfig.$fields, errors);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async _getClientForCursor() {
|
|
306
|
+
await this.initialize();
|
|
307
|
+
if (this.pool) {
|
|
308
|
+
const client = await this.pool.connect();
|
|
309
|
+
return {
|
|
310
|
+
client,
|
|
311
|
+
release: () => client.release()
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
else if (this.client) {
|
|
315
|
+
return {
|
|
316
|
+
client: this.client,
|
|
317
|
+
release: () => { }
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
throw new RelqConnectionError('No database connection available');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
get [INTERNAL]() {
|
|
325
|
+
return {
|
|
326
|
+
executeSelect: this._executeSelect.bind(this),
|
|
327
|
+
executeSelectOne: this._executeSelectOne.bind(this),
|
|
328
|
+
executeCount: this._executeCount.bind(this),
|
|
329
|
+
executeRun: this._executeRun.bind(this),
|
|
330
|
+
executeQuery: this._executeQuery.bind(this),
|
|
331
|
+
transformToDbColumns: this._transformToDbColumns.bind(this),
|
|
332
|
+
transformFromDbColumns: this._transformFromDbColumns.bind(this),
|
|
333
|
+
transformResultsFromDb: this._transformResultsFromDb.bind(this),
|
|
334
|
+
hasColumnMapping: this._hasColumnMapping.bind(this),
|
|
335
|
+
getClientForCursor: this._getClientForCursor.bind(this),
|
|
336
|
+
validateData: this._validateData.bind(this)
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
determinePoolingStrategy(config) {
|
|
340
|
+
if (typeof config.pooling === 'boolean') {
|
|
341
|
+
return config.pooling;
|
|
342
|
+
}
|
|
343
|
+
if (config.disableSmartDefaults) {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
return this.environment.type === 'traditional';
|
|
347
|
+
}
|
|
348
|
+
validateConfiguration() {
|
|
349
|
+
if (this.config.disableSmartDefaults)
|
|
350
|
+
return;
|
|
351
|
+
if (this.environment.type === 'edge') {
|
|
352
|
+
throw new RelqEnvironmentError('Cannot use Relq with database connections in edge runtime.\n' +
|
|
353
|
+
'Edge runtimes (Cloudflare Workers, Deno Deploy) do not support TCP connections.\n\n' +
|
|
354
|
+
'Options:\n' +
|
|
355
|
+
'1. Use query builder only: relq("table").select().toString()\n' +
|
|
356
|
+
'2. Use HTTP-based database (Supabase, Neon, Xata)\n' +
|
|
357
|
+
'3. Deploy to Node.js runtime', getEnvironmentDescription(this.environment), 'No TCP/PostgreSQL support');
|
|
358
|
+
}
|
|
359
|
+
const poolConfig = {
|
|
360
|
+
min: this.config.pool?.min,
|
|
361
|
+
max: this.config.pool?.max
|
|
362
|
+
};
|
|
363
|
+
const validation = validatePoolConfig(poolConfig, this.environment);
|
|
364
|
+
if (!validation.valid && validation.errors.length > 0) {
|
|
365
|
+
const logLevel = this.config.logLevel ?? 'info';
|
|
366
|
+
if (logLevel !== 'silent') {
|
|
367
|
+
console.error('\n' + validation.errors.join('\n\n'));
|
|
368
|
+
}
|
|
369
|
+
throw new RelqConfigError(validation.errors[0]);
|
|
370
|
+
}
|
|
371
|
+
if (validation.warnings.length > 0) {
|
|
372
|
+
const logLevel = this.config.logLevel ?? 'info';
|
|
373
|
+
if (logLevel === 'warn' || logLevel === 'info' || logLevel === 'debug') {
|
|
374
|
+
console.warn('\n' + validation.warnings.join('\n\n'));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
logEnvironmentInfo() {
|
|
379
|
+
const logLevel = this.config.logLevel ?? 'info';
|
|
380
|
+
if (logLevel === 'silent' || logLevel === 'error')
|
|
381
|
+
return;
|
|
382
|
+
const smartDefaults = getSmartPoolDefaults();
|
|
383
|
+
const poolConfig = formatPoolConfig({
|
|
384
|
+
min: this.config.pool?.min ?? smartDefaults.min,
|
|
385
|
+
max: this.config.pool?.max ?? smartDefaults.max,
|
|
386
|
+
idleTimeoutMillis: this.config.pool?.idleTimeoutMillis ?? smartDefaults.idleTimeoutMillis,
|
|
387
|
+
connectionTimeoutMillis: this.config.pool?.connectionTimeoutMillis ?? smartDefaults.connectionTimeoutMillis
|
|
388
|
+
});
|
|
389
|
+
if (this.environment.type === 'serverless') {
|
|
390
|
+
if (logLevel === 'info' || logLevel === 'debug') {
|
|
391
|
+
console.log(`\n🔵 Relq: ${this.environment.provider} detected (serverless)\n` +
|
|
392
|
+
` Pooling: ${this.usePooling ? 'enabled' : 'disabled'}\n` +
|
|
393
|
+
` Pool config: ${poolConfig}\n` +
|
|
394
|
+
` Recommendation: Use min: 0, max: 1 for serverless`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else if (this.environment.type === 'traditional') {
|
|
398
|
+
if (logLevel === 'debug') {
|
|
399
|
+
console.log(`\n🟢 Relq: Traditional server environment\n` +
|
|
400
|
+
` Pooling: ${this.usePooling ? 'enabled' : 'disabled'}\n` +
|
|
401
|
+
` Pool config: ${poolConfig}\n` +
|
|
402
|
+
` Connections created on demand, scale up under load`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async initialize() {
|
|
407
|
+
if (this.initialized)
|
|
408
|
+
return;
|
|
409
|
+
if (this.initPromise) {
|
|
410
|
+
return this.initPromise;
|
|
411
|
+
}
|
|
412
|
+
this.initPromise = (async () => {
|
|
413
|
+
const { Pool: PgPoolClass, Client: PgClientClass } = await loadPg();
|
|
414
|
+
if (this.usePooling) {
|
|
415
|
+
this.pool = new PgPoolClass(toPoolConfig(this.config));
|
|
416
|
+
this.setupPoolErrorHandling();
|
|
417
|
+
this.pool.on('connect', (client) => {
|
|
418
|
+
this.emitter.emit('connect', client);
|
|
419
|
+
this.emitter.emit('acquire', client);
|
|
420
|
+
});
|
|
421
|
+
this.pool.on('error', (err) => this.emitter.emit('error', err));
|
|
422
|
+
this.pool.on('remove', (client) => this.emitter.emit('remove', client));
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
this.client = new PgClientClass({
|
|
426
|
+
host: this.config.host || 'localhost',
|
|
427
|
+
port: this.config.port || 5432,
|
|
428
|
+
database: this.config.database,
|
|
429
|
+
user: this.config.user,
|
|
430
|
+
password: this.config.password,
|
|
431
|
+
connectionString: this.config.connectionString,
|
|
432
|
+
ssl: this.config.ssl
|
|
433
|
+
});
|
|
434
|
+
this.client.on('end', () => this.emitter.emit('end'));
|
|
435
|
+
this.client.on('error', (err) => this.emitter.emit('error', err));
|
|
436
|
+
this.client.on('notice', (msg) => this.emitter.emit('notice', msg));
|
|
437
|
+
this.client.on('notification', (msg) => this.emitter.emit('notification', msg));
|
|
438
|
+
}
|
|
439
|
+
this.initialized = true;
|
|
440
|
+
})();
|
|
441
|
+
return this.initPromise;
|
|
442
|
+
}
|
|
443
|
+
setupPoolErrorHandling() {
|
|
444
|
+
if (!this.pool)
|
|
445
|
+
return;
|
|
446
|
+
this.poolErrorHandler = (err) => {
|
|
447
|
+
const errorCode = err.code;
|
|
448
|
+
const recoverableErrors = [
|
|
449
|
+
'ETIMEDOUT',
|
|
450
|
+
'ECONNRESET',
|
|
451
|
+
'57P02'
|
|
452
|
+
];
|
|
453
|
+
if (!recoverableErrors.includes(errorCode)) {
|
|
454
|
+
throw err;
|
|
455
|
+
}
|
|
456
|
+
console.warn('[Relq Pool] Recoverable connection error (pool will auto-recover):', {
|
|
457
|
+
code: errorCode,
|
|
458
|
+
message: err.message,
|
|
459
|
+
action: 'Connection removed from pool, will be replaced on next query'
|
|
460
|
+
});
|
|
461
|
+
};
|
|
462
|
+
this.poolConnectHandler = () => {
|
|
463
|
+
};
|
|
464
|
+
this.poolRemoveHandler = () => {
|
|
465
|
+
};
|
|
466
|
+
this.pool.on('error', this.poolErrorHandler);
|
|
467
|
+
this.pool.on('connect', this.poolConnectHandler);
|
|
468
|
+
this.pool.on('remove', this.poolRemoveHandler);
|
|
469
|
+
}
|
|
470
|
+
async connect() {
|
|
471
|
+
await this.initialize();
|
|
472
|
+
if (!this.usePooling && this.client) {
|
|
473
|
+
await this.client.connect();
|
|
474
|
+
this.emitter.emit('connect', this.client);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async subscribe(channel, callback) {
|
|
478
|
+
if (!this.listener) {
|
|
479
|
+
this.listener = new ListenerConnection({
|
|
480
|
+
host: this.config.host,
|
|
481
|
+
port: this.config.port,
|
|
482
|
+
user: this.config.user,
|
|
483
|
+
password: this.config.password,
|
|
484
|
+
database: this.config.database,
|
|
485
|
+
connectionString: this.config.connectionString,
|
|
486
|
+
ssl: this.config.ssl
|
|
487
|
+
});
|
|
488
|
+
this.listener.on('error', (err) => {
|
|
489
|
+
this.emitter.emit('listenerError', err);
|
|
490
|
+
});
|
|
491
|
+
this.listener.on('connect', () => {
|
|
492
|
+
this.emitter.emit('listenerConnect');
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
const sub = await this.listener.subscribe(channel);
|
|
496
|
+
sub.on('notification', callback);
|
|
497
|
+
return () => sub.close();
|
|
498
|
+
}
|
|
499
|
+
async close() {
|
|
500
|
+
if (!this.initialized || this.isClosed)
|
|
501
|
+
return;
|
|
502
|
+
try {
|
|
503
|
+
if (this.listener) {
|
|
504
|
+
await this.listener.close();
|
|
505
|
+
this.listener = null;
|
|
506
|
+
}
|
|
507
|
+
if (this.pool) {
|
|
508
|
+
if (this.poolErrorHandler) {
|
|
509
|
+
this.pool.removeListener('error', this.poolErrorHandler);
|
|
510
|
+
}
|
|
511
|
+
if (this.poolConnectHandler) {
|
|
512
|
+
this.pool.removeListener('connect', this.poolConnectHandler);
|
|
513
|
+
}
|
|
514
|
+
if (this.poolRemoveHandler) {
|
|
515
|
+
this.pool.removeListener('remove', this.poolRemoveHandler);
|
|
516
|
+
}
|
|
517
|
+
this.pool.removeAllListeners();
|
|
518
|
+
await this.pool.end();
|
|
519
|
+
}
|
|
520
|
+
else if (this.client) {
|
|
521
|
+
this.client.removeAllListeners();
|
|
522
|
+
await this.client.end();
|
|
523
|
+
}
|
|
524
|
+
this.isClosed = true;
|
|
525
|
+
activeRelqInstances.delete(this);
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
console.error('Error closing database connection:', error);
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
get closed() {
|
|
533
|
+
return this.isClosed;
|
|
534
|
+
}
|
|
535
|
+
on(event, listener) {
|
|
536
|
+
this.emitter.on(event, listener);
|
|
537
|
+
return this;
|
|
538
|
+
}
|
|
539
|
+
once(event, listener) {
|
|
540
|
+
this.emitter.once(event, listener);
|
|
541
|
+
return this;
|
|
542
|
+
}
|
|
543
|
+
off(event, listener) {
|
|
544
|
+
this.emitter.off(event, listener);
|
|
545
|
+
return this;
|
|
546
|
+
}
|
|
547
|
+
async _executeQuery(sql) {
|
|
548
|
+
try {
|
|
549
|
+
if (!sql || typeof sql !== 'string' || sql.trim() === '') {
|
|
550
|
+
throw new RelqConfigError(`Invalid SQL query: ${sql === null ? 'null' : sql === undefined ? 'undefined' : 'empty string'}`);
|
|
551
|
+
}
|
|
552
|
+
await this.initialize();
|
|
553
|
+
const startTime = performance.now();
|
|
554
|
+
let result;
|
|
555
|
+
if (this.pool) {
|
|
556
|
+
result = await this.pool.query(sql);
|
|
557
|
+
}
|
|
558
|
+
else if (this.client) {
|
|
559
|
+
result = await this.client.query(sql);
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
throw new RelqConfigError('No database connection available');
|
|
563
|
+
}
|
|
564
|
+
const duration = performance.now() - startTime;
|
|
565
|
+
return { result, duration };
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
throw parsePostgresError(error, sql);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
buildMetadata(result, duration) {
|
|
572
|
+
return {
|
|
573
|
+
rowCount: result.rowCount,
|
|
574
|
+
command: result.command,
|
|
575
|
+
duration,
|
|
576
|
+
fields: result.fields
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
async _executeSelect(sql, tableName) {
|
|
580
|
+
const { result, duration } = await this._executeQuery(sql);
|
|
581
|
+
const rows = tableName
|
|
582
|
+
? this._transformResultsFromDb(tableName, result.rows)
|
|
583
|
+
: result.rows;
|
|
584
|
+
return {
|
|
585
|
+
data: rows,
|
|
586
|
+
metadata: this.buildMetadata(result, duration)
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
async _executeSelectOne(sql, tableName) {
|
|
590
|
+
const { result, duration } = await this._executeQuery(sql);
|
|
591
|
+
const row = result.rows[0]
|
|
592
|
+
? (tableName ? this._transformFromDbColumns(tableName, result.rows[0]) : result.rows[0])
|
|
593
|
+
: null;
|
|
594
|
+
return {
|
|
595
|
+
data: row,
|
|
596
|
+
metadata: this.buildMetadata(result, duration)
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
async _executeCount(sql) {
|
|
600
|
+
const { result, duration } = await this._executeQuery(sql);
|
|
601
|
+
const count = result.rows[0]?.count
|
|
602
|
+
? parseInt(result.rows[0].count, 10)
|
|
603
|
+
: 0;
|
|
604
|
+
return {
|
|
605
|
+
count,
|
|
606
|
+
metadata: this.buildMetadata(result, duration)
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
async _executeRun(sql) {
|
|
610
|
+
const { result, duration } = await this._executeQuery(sql);
|
|
611
|
+
return {
|
|
612
|
+
success: true,
|
|
613
|
+
metadata: this.buildMetadata(result, duration)
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
get table() {
|
|
617
|
+
const relq = this;
|
|
618
|
+
const tableFunction = (tableName) => {
|
|
619
|
+
return new ConnectedQueryBuilder(tableName, relq);
|
|
620
|
+
};
|
|
621
|
+
return new Proxy(tableFunction, {
|
|
622
|
+
get(target, prop, receiver) {
|
|
623
|
+
if (prop in target) {
|
|
624
|
+
return Reflect.get(target, prop, receiver);
|
|
625
|
+
}
|
|
626
|
+
if (typeof prop === 'string' && relq.schema && prop in relq.schema) {
|
|
627
|
+
const tableDef = relq.schema[prop];
|
|
628
|
+
const sqlTableName = tableDef?.$name || prop;
|
|
629
|
+
return new ConnectedQueryBuilder(sqlTableName, relq);
|
|
630
|
+
}
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
raw(query, ...params) {
|
|
636
|
+
return new ConnectedRawQueryBuilder(query, params, this);
|
|
637
|
+
}
|
|
638
|
+
transaction() {
|
|
639
|
+
return new ConnectedTransactionBuilder(this);
|
|
640
|
+
}
|
|
641
|
+
with(name, query) {
|
|
642
|
+
return new ConnectedCTEBuilder(this).with(name, query);
|
|
643
|
+
}
|
|
644
|
+
async ctAs(tableName, query, options = {}) {
|
|
645
|
+
const queryStr = typeof query === 'string' ? query : query.toString();
|
|
646
|
+
const temp = options.temporary ? 'TEMPORARY ' : '';
|
|
647
|
+
const ifNotExists = options.ifNotExists ? 'IF NOT EXISTS ' : '';
|
|
648
|
+
const sql = `CREATE ${temp}TABLE ${ifNotExists}"${tableName}" AS ${queryStr}`;
|
|
649
|
+
await this[INTERNAL].executeQuery(sql);
|
|
650
|
+
}
|
|
651
|
+
async explain(query, options = {}) {
|
|
652
|
+
const queryStr = typeof query === 'string' ? query : query.toString();
|
|
653
|
+
const parts = ['EXPLAIN'];
|
|
654
|
+
if (options.format)
|
|
655
|
+
parts.push(`(FORMAT ${options.format.toUpperCase()}`);
|
|
656
|
+
if (options.analyze)
|
|
657
|
+
parts.push(options.format ? ', ANALYZE' : '(ANALYZE');
|
|
658
|
+
if (options.verbose)
|
|
659
|
+
parts.push(options.format || options.analyze ? ', VERBOSE' : '(VERBOSE');
|
|
660
|
+
if (options.format || options.analyze || options.verbose)
|
|
661
|
+
parts.push(')');
|
|
662
|
+
const sql = `${parts.join(' ')} ${queryStr}`;
|
|
663
|
+
const result = await this[INTERNAL].executeQuery(sql);
|
|
664
|
+
if (options.format === 'json') {
|
|
665
|
+
return result.result.rows;
|
|
666
|
+
}
|
|
667
|
+
return result.result.rows.map((r) => r['QUERY PLAN'] || Object.values(r)[0]).join('\n');
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
class ConnectedQueryBuilder {
|
|
671
|
+
tableName;
|
|
672
|
+
relq;
|
|
673
|
+
constructor(tableName, relq) {
|
|
674
|
+
this.tableName = tableName;
|
|
675
|
+
this.relq = relq;
|
|
676
|
+
}
|
|
677
|
+
select(columns) {
|
|
678
|
+
let dbColumns = columns;
|
|
679
|
+
if (columns && this.relq[INTERNAL].hasColumnMapping()) {
|
|
680
|
+
if (Array.isArray(columns)) {
|
|
681
|
+
dbColumns = columns.map(col => {
|
|
682
|
+
if (Array.isArray(col)) {
|
|
683
|
+
const transformed = this.relq[INTERNAL].transformToDbColumns(this.tableName, { [col[0]]: true });
|
|
684
|
+
return [Object.keys(transformed)[0], col[1]];
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
const transformed = this.relq[INTERNAL].transformToDbColumns(this.tableName, { [col]: true });
|
|
688
|
+
return Object.keys(transformed)[0];
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
const transformed = this.relq[INTERNAL].transformToDbColumns(this.tableName, { [columns]: true });
|
|
694
|
+
dbColumns = Object.keys(transformed)[0];
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const builder = new SelectBuilder(this.tableName, dbColumns);
|
|
698
|
+
return new ConnectedSelectBuilder(builder, this.relq, this.tableName, columns);
|
|
699
|
+
}
|
|
700
|
+
insert(data) {
|
|
701
|
+
const builder = new InsertBuilder(this.tableName, data);
|
|
702
|
+
return new ConnectedInsertBuilder(builder, this.relq);
|
|
703
|
+
}
|
|
704
|
+
update(data) {
|
|
705
|
+
const builder = new UpdateBuilder(this.tableName, data);
|
|
706
|
+
return new ConnectedUpdateBuilder(builder, this.relq);
|
|
707
|
+
}
|
|
708
|
+
delete() {
|
|
709
|
+
const builder = new DeleteBuilder(this.tableName);
|
|
710
|
+
return new ConnectedDeleteBuilder(builder, this.relq);
|
|
711
|
+
}
|
|
712
|
+
count() {
|
|
713
|
+
const builder = new CountBuilder(this.tableName);
|
|
714
|
+
return new ConnectedCountBuilder(builder, this.relq, this.tableName);
|
|
715
|
+
}
|
|
716
|
+
aggregate() {
|
|
717
|
+
const builder = new AggregateQueryBuilder(this.tableName);
|
|
718
|
+
return new ConnectedAggregateBuilder(builder, this.relq, this.tableName);
|
|
719
|
+
}
|
|
720
|
+
async findById(id) {
|
|
721
|
+
const pkColumn = this.getPrimaryKeyColumn();
|
|
722
|
+
const dbColumn = this.relq[INTERNAL].transformToDbColumns(this.tableName, { [pkColumn]: id });
|
|
723
|
+
const dbColName = Object.keys(dbColumn)[0];
|
|
724
|
+
const builder = new SelectBuilder(this.tableName, ['*']);
|
|
725
|
+
builder.where(q => q.equal(dbColName, id));
|
|
726
|
+
builder.limit(1);
|
|
727
|
+
const sql = builder.toString();
|
|
728
|
+
const result = await this.relq[INTERNAL].executeSelectOne(sql, this.tableName);
|
|
729
|
+
return result.data;
|
|
730
|
+
}
|
|
731
|
+
async findOne(filter) {
|
|
732
|
+
const builder = new SelectBuilder(this.tableName, ['*']);
|
|
733
|
+
const dbFilter = this.relq[INTERNAL].transformToDbColumns(this.tableName, filter);
|
|
734
|
+
builder.where(q => {
|
|
735
|
+
for (const [col, val] of Object.entries(dbFilter)) {
|
|
736
|
+
q.equal(col, val);
|
|
737
|
+
}
|
|
738
|
+
return q;
|
|
739
|
+
});
|
|
740
|
+
builder.limit(1);
|
|
741
|
+
const sql = builder.toString();
|
|
742
|
+
const result = await this.relq[INTERNAL].executeSelectOne(sql, this.tableName);
|
|
743
|
+
return result.data;
|
|
744
|
+
}
|
|
745
|
+
async findMany(filter) {
|
|
746
|
+
const builder = new SelectBuilder(this.tableName, ['*']);
|
|
747
|
+
const dbFilter = this.relq[INTERNAL].transformToDbColumns(this.tableName, filter);
|
|
748
|
+
builder.where(q => {
|
|
749
|
+
for (const [col, val] of Object.entries(dbFilter)) {
|
|
750
|
+
q.equal(col, val);
|
|
751
|
+
}
|
|
752
|
+
return q;
|
|
753
|
+
});
|
|
754
|
+
const sql = builder.toString();
|
|
755
|
+
const result = await this.relq[INTERNAL].executeSelect(sql, this.tableName);
|
|
756
|
+
return result.data;
|
|
757
|
+
}
|
|
758
|
+
async exists(filter) {
|
|
759
|
+
const builder = new CountBuilder(this.tableName);
|
|
760
|
+
const dbFilter = this.relq[INTERNAL].transformToDbColumns(this.tableName, filter);
|
|
761
|
+
builder.where(q => {
|
|
762
|
+
for (const [col, val] of Object.entries(dbFilter)) {
|
|
763
|
+
q.equal(col, val);
|
|
764
|
+
}
|
|
765
|
+
return q;
|
|
766
|
+
});
|
|
767
|
+
const sql = builder.toString();
|
|
768
|
+
const result = await this.relq[INTERNAL].executeCount(sql);
|
|
769
|
+
return result.count > 0;
|
|
770
|
+
}
|
|
771
|
+
async upsert(options) {
|
|
772
|
+
const dbCreate = this.relq[INTERNAL].transformToDbColumns(this.tableName, options.create);
|
|
773
|
+
const dbUpdate = this.relq[INTERNAL].transformToDbColumns(this.tableName, options.update);
|
|
774
|
+
const dbWhere = this.relq[INTERNAL].transformToDbColumns(this.tableName, options.where);
|
|
775
|
+
const conflictColumn = Object.keys(dbWhere)[0];
|
|
776
|
+
const builder = new InsertBuilder(this.tableName, dbCreate);
|
|
777
|
+
builder.onConflict(conflictColumn, cb => cb.doUpdate(dbUpdate));
|
|
778
|
+
builder.returning(['*']);
|
|
779
|
+
const sql = builder.toString();
|
|
780
|
+
const result = await this.relq[INTERNAL].executeQuery(sql);
|
|
781
|
+
const transformed = this.relq[INTERNAL].transformResultsFromDb(this.tableName, result.result.rows);
|
|
782
|
+
return transformed[0];
|
|
783
|
+
}
|
|
784
|
+
async insertMany(rows) {
|
|
785
|
+
if (rows.length === 0)
|
|
786
|
+
return [];
|
|
787
|
+
const firstDbRow = this.relq[INTERNAL].transformToDbColumns(this.tableName, rows[0]);
|
|
788
|
+
const builder = new InsertBuilder(this.tableName, firstDbRow);
|
|
789
|
+
for (let i = 1; i < rows.length; i++) {
|
|
790
|
+
const dbRow = this.relq[INTERNAL].transformToDbColumns(this.tableName, rows[i]);
|
|
791
|
+
builder.addRow(dbRow);
|
|
792
|
+
}
|
|
793
|
+
builder.returning(['*']);
|
|
794
|
+
const sql = builder.toString();
|
|
795
|
+
const result = await this.relq[INTERNAL].executeQuery(sql);
|
|
796
|
+
return this.relq[INTERNAL].transformResultsFromDb(this.tableName, result.result.rows);
|
|
797
|
+
}
|
|
798
|
+
paginate(options = {}) {
|
|
799
|
+
return new PaginateBuilder(this.relq, this.tableName, options.columns, options.where, options.orderBy);
|
|
800
|
+
}
|
|
801
|
+
async softDelete(filter) {
|
|
802
|
+
const dbFilter = this.relq[INTERNAL].transformToDbColumns(this.tableName, filter);
|
|
803
|
+
const builder = new UpdateBuilder(this.tableName, { deleted_at: new Date() });
|
|
804
|
+
builder.where(q => {
|
|
805
|
+
for (const [col, val] of Object.entries(dbFilter)) {
|
|
806
|
+
q.equal(col, val);
|
|
807
|
+
}
|
|
808
|
+
return q;
|
|
809
|
+
});
|
|
810
|
+
builder.returning(['*']);
|
|
811
|
+
const sql = builder.toString();
|
|
812
|
+
const result = await this.relq[INTERNAL].executeQuery(sql);
|
|
813
|
+
if (result.result.rows.length === 0)
|
|
814
|
+
return null;
|
|
815
|
+
const transformed = this.relq[INTERNAL].transformResultsFromDb(this.tableName, result.result.rows);
|
|
816
|
+
return transformed[0];
|
|
817
|
+
}
|
|
818
|
+
async restore(filter) {
|
|
819
|
+
const dbFilter = this.relq[INTERNAL].transformToDbColumns(this.tableName, filter);
|
|
820
|
+
const builder = new UpdateBuilder(this.tableName, { deleted_at: null });
|
|
821
|
+
builder.where(q => {
|
|
822
|
+
for (const [col, val] of Object.entries(dbFilter)) {
|
|
823
|
+
q.equal(col, val);
|
|
824
|
+
}
|
|
825
|
+
return q;
|
|
826
|
+
});
|
|
827
|
+
builder.returning(['*']);
|
|
828
|
+
const sql = builder.toString();
|
|
829
|
+
const result = await this.relq[INTERNAL].executeQuery(sql);
|
|
830
|
+
if (result.result.rows.length === 0)
|
|
831
|
+
return null;
|
|
832
|
+
const transformed = this.relq[INTERNAL].transformResultsFromDb(this.tableName, result.result.rows);
|
|
833
|
+
return transformed[0];
|
|
834
|
+
}
|
|
835
|
+
getPrimaryKeyColumn() {
|
|
836
|
+
const schema = this.relq.schema;
|
|
837
|
+
if (schema && schema[this.tableName]) {
|
|
838
|
+
const tableSchema = schema[this.tableName];
|
|
839
|
+
for (const [colName, colDef] of Object.entries(tableSchema)) {
|
|
840
|
+
if (colDef && typeof colDef === 'object' &&
|
|
841
|
+
('$primaryKey' in colDef ||
|
|
842
|
+
colDef.config?.$primaryKey === true)) {
|
|
843
|
+
return colName;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return 'id';
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
class ConnectedAggregateBuilder {
|
|
851
|
+
builder;
|
|
852
|
+
relq;
|
|
853
|
+
tableName;
|
|
854
|
+
constructor(builder, relq, tableName) {
|
|
855
|
+
this.builder = builder;
|
|
856
|
+
this.relq = relq;
|
|
857
|
+
this.tableName = tableName;
|
|
858
|
+
}
|
|
859
|
+
mapColumn(column) {
|
|
860
|
+
const transformed = this.relq[INTERNAL].transformToDbColumns(this.tableName, { [column]: true });
|
|
861
|
+
return Object.keys(transformed)[0] || column;
|
|
862
|
+
}
|
|
863
|
+
groupBy(...columns) {
|
|
864
|
+
const dbColumns = columns.map(col => this.mapColumn(col));
|
|
865
|
+
this.builder.groupBy(...dbColumns);
|
|
866
|
+
return this;
|
|
867
|
+
}
|
|
868
|
+
count(alias) {
|
|
869
|
+
this.builder.count(alias);
|
|
870
|
+
return this;
|
|
871
|
+
}
|
|
872
|
+
countColumn(column, alias) {
|
|
873
|
+
this.builder.countColumn(this.mapColumn(column), alias);
|
|
874
|
+
return this;
|
|
875
|
+
}
|
|
876
|
+
countDistinct(column, alias) {
|
|
877
|
+
this.builder.countDistinct(this.mapColumn(column), alias);
|
|
878
|
+
return this;
|
|
879
|
+
}
|
|
880
|
+
sum(column, alias) {
|
|
881
|
+
this.builder.sum(this.mapColumn(column), alias);
|
|
882
|
+
return this;
|
|
883
|
+
}
|
|
884
|
+
avg(column, alias) {
|
|
885
|
+
this.builder.avg(this.mapColumn(column), alias);
|
|
886
|
+
return this;
|
|
887
|
+
}
|
|
888
|
+
min(column, alias) {
|
|
889
|
+
this.builder.min(this.mapColumn(column), alias);
|
|
890
|
+
return this;
|
|
891
|
+
}
|
|
892
|
+
max(column, alias) {
|
|
893
|
+
this.builder.max(this.mapColumn(column), alias);
|
|
894
|
+
return this;
|
|
895
|
+
}
|
|
896
|
+
arrayAgg(column, alias) {
|
|
897
|
+
this.builder.arrayAgg(this.mapColumn(column), alias);
|
|
898
|
+
return this;
|
|
899
|
+
}
|
|
900
|
+
stringAgg(column, delimiter, alias) {
|
|
901
|
+
this.builder.stringAgg(this.mapColumn(column), delimiter || ', ', alias);
|
|
902
|
+
return this;
|
|
903
|
+
}
|
|
904
|
+
jsonAgg(column, alias) {
|
|
905
|
+
this.builder.jsonAgg(this.mapColumn(column), alias);
|
|
906
|
+
return this;
|
|
907
|
+
}
|
|
908
|
+
jsonbAgg(column, alias) {
|
|
909
|
+
this.builder.jsonbAgg(this.mapColumn(column), alias);
|
|
910
|
+
return this;
|
|
911
|
+
}
|
|
912
|
+
boolAnd(column, alias) {
|
|
913
|
+
this.builder.boolAnd(this.mapColumn(column), alias);
|
|
914
|
+
return this;
|
|
915
|
+
}
|
|
916
|
+
boolOr(column, alias) {
|
|
917
|
+
this.builder.boolOr(this.mapColumn(column), alias);
|
|
918
|
+
return this;
|
|
919
|
+
}
|
|
920
|
+
where(callback) {
|
|
921
|
+
this.builder.where(callback);
|
|
922
|
+
return this;
|
|
923
|
+
}
|
|
924
|
+
having(callback) {
|
|
925
|
+
this.builder.having(callback);
|
|
926
|
+
return this;
|
|
927
|
+
}
|
|
928
|
+
orderBy(column, direction = 'ASC') {
|
|
929
|
+
this.builder.orderBy(this.mapColumn(column), direction);
|
|
930
|
+
return this;
|
|
931
|
+
}
|
|
932
|
+
limit(count) {
|
|
933
|
+
this.builder.limit(count);
|
|
934
|
+
return this;
|
|
935
|
+
}
|
|
936
|
+
offset(count) {
|
|
937
|
+
this.builder.offset(count);
|
|
938
|
+
return this;
|
|
939
|
+
}
|
|
940
|
+
toString() {
|
|
941
|
+
return this.builder.toString();
|
|
942
|
+
}
|
|
943
|
+
async all() {
|
|
944
|
+
const sql = this.builder.toString();
|
|
945
|
+
const result = await this.relq[INTERNAL].executeSelect(sql, this.tableName);
|
|
946
|
+
const numericAliases = this.builder.getNumericAliases();
|
|
947
|
+
if (numericAliases.length > 0) {
|
|
948
|
+
for (const row of result.data) {
|
|
949
|
+
for (const alias of numericAliases) {
|
|
950
|
+
if (alias in row && typeof row[alias] === 'string') {
|
|
951
|
+
row[alias] = Number(row[alias]);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return result.data;
|
|
957
|
+
}
|
|
958
|
+
async get() {
|
|
959
|
+
this.builder.limit(1);
|
|
960
|
+
const sql = this.builder.toString();
|
|
961
|
+
const result = await this.relq[INTERNAL].executeSelectOne(sql, this.tableName);
|
|
962
|
+
const numericAliases = this.builder.getNumericAliases();
|
|
963
|
+
if (numericAliases.length > 0 && result.data) {
|
|
964
|
+
for (const alias of numericAliases) {
|
|
965
|
+
if (alias in result.data && typeof result.data[alias] === 'string') {
|
|
966
|
+
result.data[alias] = Number(result.data[alias]);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return result.data;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
class ConnectedSelectBuilder {
|
|
974
|
+
builder;
|
|
975
|
+
relq;
|
|
976
|
+
tableName;
|
|
977
|
+
columns;
|
|
978
|
+
constructor(builder, relq, tableName, columns) {
|
|
979
|
+
this.builder = builder;
|
|
980
|
+
this.relq = relq;
|
|
981
|
+
this.tableName = tableName;
|
|
982
|
+
this.columns = columns;
|
|
983
|
+
}
|
|
984
|
+
where(callback) {
|
|
985
|
+
this.builder.where(callback);
|
|
986
|
+
return this;
|
|
987
|
+
}
|
|
988
|
+
orderBy(column, direction) {
|
|
989
|
+
const dbColumn = this.relq[INTERNAL].hasColumnMapping()
|
|
990
|
+
? Object.keys(this.relq[INTERNAL].transformToDbColumns(this.tableName, { [column]: true }))[0]
|
|
991
|
+
: column;
|
|
992
|
+
this.builder.orderBy(dbColumn, direction);
|
|
993
|
+
return this;
|
|
994
|
+
}
|
|
995
|
+
limit(count) {
|
|
996
|
+
this.builder.limit(count);
|
|
997
|
+
return this;
|
|
998
|
+
}
|
|
999
|
+
offset(count) {
|
|
1000
|
+
this.builder.offset(count);
|
|
1001
|
+
return this;
|
|
1002
|
+
}
|
|
1003
|
+
groupBy(...columns) {
|
|
1004
|
+
const dbColumns = this.relq[INTERNAL].hasColumnMapping()
|
|
1005
|
+
? columns.map(col => Object.keys(this.relq[INTERNAL].transformToDbColumns(this.tableName, { [col]: true }))[0])
|
|
1006
|
+
: columns;
|
|
1007
|
+
this.builder.groupBy(...dbColumns);
|
|
1008
|
+
return this;
|
|
1009
|
+
}
|
|
1010
|
+
having(callback) {
|
|
1011
|
+
this.builder.having(callback);
|
|
1012
|
+
return this;
|
|
1013
|
+
}
|
|
1014
|
+
join(table, condition) {
|
|
1015
|
+
this.builder.join(table, condition);
|
|
1016
|
+
return this;
|
|
1017
|
+
}
|
|
1018
|
+
leftJoin(table, condition) {
|
|
1019
|
+
this.builder.leftJoin(table, condition);
|
|
1020
|
+
return this;
|
|
1021
|
+
}
|
|
1022
|
+
rightJoin(table, condition) {
|
|
1023
|
+
this.builder.rightJoin(table, condition);
|
|
1024
|
+
return this;
|
|
1025
|
+
}
|
|
1026
|
+
innerJoin(table, condition) {
|
|
1027
|
+
this.builder.innerJoin(table, condition);
|
|
1028
|
+
return this;
|
|
1029
|
+
}
|
|
1030
|
+
distinct() {
|
|
1031
|
+
this.builder.distinct();
|
|
1032
|
+
return this;
|
|
1033
|
+
}
|
|
1034
|
+
distinctOn(...columns) {
|
|
1035
|
+
this.builder.distinctOn(...columns);
|
|
1036
|
+
return this;
|
|
1037
|
+
}
|
|
1038
|
+
union(query) {
|
|
1039
|
+
this.builder.union(typeof query === 'string' ? query : query.toString());
|
|
1040
|
+
return this;
|
|
1041
|
+
}
|
|
1042
|
+
unionAll(query) {
|
|
1043
|
+
this.builder.unionAll(typeof query === 'string' ? query : query.toString());
|
|
1044
|
+
return this;
|
|
1045
|
+
}
|
|
1046
|
+
intersect(query) {
|
|
1047
|
+
this.builder.intersect(typeof query === 'string' ? query : query.toString());
|
|
1048
|
+
return this;
|
|
1049
|
+
}
|
|
1050
|
+
except(query) {
|
|
1051
|
+
this.builder.except(typeof query === 'string' ? query : query.toString());
|
|
1052
|
+
return this;
|
|
1053
|
+
}
|
|
1054
|
+
forUpdate() {
|
|
1055
|
+
this.builder.forUpdate();
|
|
1056
|
+
return this;
|
|
1057
|
+
}
|
|
1058
|
+
forUpdateSkipLocked() {
|
|
1059
|
+
this.builder.forUpdateSkipLocked();
|
|
1060
|
+
return this;
|
|
1061
|
+
}
|
|
1062
|
+
forShare() {
|
|
1063
|
+
this.builder.forShare();
|
|
1064
|
+
return this;
|
|
1065
|
+
}
|
|
1066
|
+
toString() {
|
|
1067
|
+
return this.builder.toString();
|
|
1068
|
+
}
|
|
1069
|
+
async all(withMetadata, _asRequired) {
|
|
1070
|
+
const sql = this.builder.toString();
|
|
1071
|
+
const result = await this.relq[INTERNAL].executeSelect(sql, this.tableName);
|
|
1072
|
+
if (withMetadata) {
|
|
1073
|
+
return result;
|
|
1074
|
+
}
|
|
1075
|
+
return result.data;
|
|
1076
|
+
}
|
|
1077
|
+
async get(withMetadata, _asRequired) {
|
|
1078
|
+
this.builder.limit(1);
|
|
1079
|
+
const sql = this.builder.toString();
|
|
1080
|
+
const result = await this.relq[INTERNAL].executeSelectOne(sql, this.tableName);
|
|
1081
|
+
if (withMetadata) {
|
|
1082
|
+
return result;
|
|
1083
|
+
}
|
|
1084
|
+
return result.data;
|
|
1085
|
+
}
|
|
1086
|
+
async value(column) {
|
|
1087
|
+
this.builder.limit(1);
|
|
1088
|
+
const sql = this.builder.toString();
|
|
1089
|
+
const result = await this.relq[INTERNAL].executeSelectOne(sql, this.tableName);
|
|
1090
|
+
return result.data?.[column] ?? null;
|
|
1091
|
+
}
|
|
1092
|
+
async each(callback, options = {}) {
|
|
1093
|
+
const batchSize = options.batchSize ?? 100;
|
|
1094
|
+
const sql = this.builder.toString();
|
|
1095
|
+
const relqAny = this.relq;
|
|
1096
|
+
if (relqAny.usePooling === false || (!relqAny.pool && relqAny.client)) {
|
|
1097
|
+
throw new RelqConfigError('each() requires pooled connections', { field: 'pooling', value: 'false (must be true)' });
|
|
1098
|
+
}
|
|
1099
|
+
const { client, release } = await this.relq[INTERNAL].getClientForCursor();
|
|
1100
|
+
try {
|
|
1101
|
+
await client.query('BEGIN');
|
|
1102
|
+
const cursor = client.query(new Cursor(sql));
|
|
1103
|
+
let rows;
|
|
1104
|
+
let aborted = false;
|
|
1105
|
+
let index = 0;
|
|
1106
|
+
outer: do {
|
|
1107
|
+
rows = await new Promise((resolve, reject) => {
|
|
1108
|
+
cursor.read(batchSize, (err, result) => {
|
|
1109
|
+
if (err)
|
|
1110
|
+
reject(err);
|
|
1111
|
+
else
|
|
1112
|
+
resolve(result);
|
|
1113
|
+
});
|
|
1114
|
+
});
|
|
1115
|
+
const transformedRows = this.tableName && this.relq._transformResultsFromDb
|
|
1116
|
+
? this.relq._transformResultsFromDb(this.tableName, rows)
|
|
1117
|
+
: rows;
|
|
1118
|
+
for (const row of transformedRows) {
|
|
1119
|
+
const result = await callback(row, index++);
|
|
1120
|
+
if (result === false) {
|
|
1121
|
+
aborted = true;
|
|
1122
|
+
break outer;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
} while (rows.length > 0);
|
|
1126
|
+
await new Promise((resolve, reject) => {
|
|
1127
|
+
cursor.close((err) => {
|
|
1128
|
+
if (err)
|
|
1129
|
+
reject(err);
|
|
1130
|
+
else
|
|
1131
|
+
resolve();
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
1134
|
+
await client.query('COMMIT');
|
|
1135
|
+
}
|
|
1136
|
+
catch (error) {
|
|
1137
|
+
await client.query('ROLLBACK');
|
|
1138
|
+
throw error;
|
|
1139
|
+
}
|
|
1140
|
+
finally {
|
|
1141
|
+
release();
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
async pagination(options) {
|
|
1145
|
+
if (!options.mode || !['paging', 'offset'].includes(options.mode)) {
|
|
1146
|
+
throw new RelqQueryError('pagination() requires "mode" to be one of: \'paging\', \'offset\'', { hint: 'Set mode: "paging" or mode: "offset"' });
|
|
1147
|
+
}
|
|
1148
|
+
const orderByArr = options.orderBy
|
|
1149
|
+
? (Array.isArray(options.orderBy[0]) ? options.orderBy : [options.orderBy])
|
|
1150
|
+
: [];
|
|
1151
|
+
for (const [column, direction] of orderByArr) {
|
|
1152
|
+
const dbColumn = this.relq[INTERNAL].hasColumnMapping()
|
|
1153
|
+
? Object.keys(this.relq[INTERNAL].transformToDbColumns(this.tableName, { [column]: true }))[0]
|
|
1154
|
+
: column;
|
|
1155
|
+
this.builder.orderBy(dbColumn, direction);
|
|
1156
|
+
}
|
|
1157
|
+
const isPaging = options.mode === 'paging';
|
|
1158
|
+
const shouldCount = options.count ?? isPaging;
|
|
1159
|
+
let total = 0;
|
|
1160
|
+
if (shouldCount) {
|
|
1161
|
+
const countSql = this.builder.toCountSQL();
|
|
1162
|
+
const countResult = await this.relq[INTERNAL].executeCount(countSql);
|
|
1163
|
+
total = countResult.count;
|
|
1164
|
+
}
|
|
1165
|
+
if (isPaging) {
|
|
1166
|
+
const { page, perPage } = options;
|
|
1167
|
+
if (typeof page !== 'number' || isNaN(page) || page < 1) {
|
|
1168
|
+
throw new RelqQueryError('pagination() paging mode requires "page" as a positive number (1-indexed)', { hint: 'page must be >= 1' });
|
|
1169
|
+
}
|
|
1170
|
+
if (typeof perPage !== 'number' || isNaN(perPage) || perPage < 1) {
|
|
1171
|
+
throw new RelqQueryError('pagination() paging mode requires "perPage" as a positive number', { hint: 'perPage must be >= 1' });
|
|
1172
|
+
}
|
|
1173
|
+
const offset = (page - 1) * perPage;
|
|
1174
|
+
this.builder.limit(perPage);
|
|
1175
|
+
this.builder.offset(offset);
|
|
1176
|
+
const sql = this.builder.toString();
|
|
1177
|
+
const result = await this.relq[INTERNAL].executeSelect(sql, this.tableName);
|
|
1178
|
+
const totalPages = Math.ceil(total / perPage);
|
|
1179
|
+
const hasNext = page < totalPages;
|
|
1180
|
+
const hasPrev = page > 1;
|
|
1181
|
+
const pagination = {
|
|
1182
|
+
page,
|
|
1183
|
+
perPage,
|
|
1184
|
+
total,
|
|
1185
|
+
totalPages,
|
|
1186
|
+
};
|
|
1187
|
+
if (hasNext) {
|
|
1188
|
+
pagination.hasNext = true;
|
|
1189
|
+
pagination.nextPage = page + 1;
|
|
1190
|
+
Object.defineProperty(pagination, 'loadNext', {
|
|
1191
|
+
value: () => this.pagination({ ...options, page: page + 1 }),
|
|
1192
|
+
enumerable: false,
|
|
1193
|
+
configurable: false
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
else {
|
|
1197
|
+
pagination.hasNext = false;
|
|
1198
|
+
}
|
|
1199
|
+
if (hasPrev) {
|
|
1200
|
+
pagination.hasPrev = true;
|
|
1201
|
+
pagination.prevPage = page - 1;
|
|
1202
|
+
Object.defineProperty(pagination, 'loadPrev', {
|
|
1203
|
+
value: () => this.pagination({ ...options, page: page - 1 }),
|
|
1204
|
+
enumerable: false,
|
|
1205
|
+
configurable: false
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
else {
|
|
1209
|
+
pagination.hasPrev = false;
|
|
1210
|
+
}
|
|
1211
|
+
const getVisibleProps = () => {
|
|
1212
|
+
const visible = { page, perPage, total, totalPages, hasNext };
|
|
1213
|
+
if (hasNext)
|
|
1214
|
+
visible.nextPage = page + 1;
|
|
1215
|
+
visible.hasPrev = hasPrev;
|
|
1216
|
+
if (hasPrev)
|
|
1217
|
+
visible.prevPage = page - 1;
|
|
1218
|
+
return visible;
|
|
1219
|
+
};
|
|
1220
|
+
Object.defineProperty(pagination, Symbol.for('nodejs.util.inspect.custom'), {
|
|
1221
|
+
value: () => getVisibleProps(),
|
|
1222
|
+
enumerable: false
|
|
1223
|
+
});
|
|
1224
|
+
Object.defineProperty(pagination, 'toJSON', {
|
|
1225
|
+
value: () => getVisibleProps(),
|
|
1226
|
+
enumerable: false
|
|
1227
|
+
});
|
|
1228
|
+
return { data: result.data, pagination };
|
|
1229
|
+
}
|
|
1230
|
+
if (options.mode === 'offset') {
|
|
1231
|
+
const { position, limit: limitOpt } = options;
|
|
1232
|
+
if (typeof position !== 'number' || isNaN(position)) {
|
|
1233
|
+
throw new RelqQueryError('pagination() offset mode requires "position" as a number', { hint: 'position must be >= 0' });
|
|
1234
|
+
}
|
|
1235
|
+
if (limitOpt === undefined || (typeof limitOpt !== 'number' && !Array.isArray(limitOpt))) {
|
|
1236
|
+
throw new RelqQueryError('pagination() offset mode requires "limit" as a number or [min, max] array', { hint: 'Use limit: number or limit: [min, max]' });
|
|
1237
|
+
}
|
|
1238
|
+
const limit = Array.isArray(limitOpt) ? randomLimit(limitOpt) : limitOpt;
|
|
1239
|
+
this.builder.limit(limit + 1);
|
|
1240
|
+
this.builder.offset(position);
|
|
1241
|
+
const sql = this.builder.toString();
|
|
1242
|
+
const result = await this.relq[INTERNAL].executeSelect(sql, this.tableName);
|
|
1243
|
+
const hasMore = result.data.length > limit;
|
|
1244
|
+
const hasPrev = position > 0;
|
|
1245
|
+
const data = hasMore ? result.data.slice(0, limit) : result.data;
|
|
1246
|
+
const pagination = {
|
|
1247
|
+
position,
|
|
1248
|
+
limit,
|
|
1249
|
+
...(shouldCount && { total }),
|
|
1250
|
+
};
|
|
1251
|
+
if (hasMore) {
|
|
1252
|
+
pagination.hasMore = true;
|
|
1253
|
+
pagination.nextPos = position + limit;
|
|
1254
|
+
Object.defineProperty(pagination, 'loadNext', {
|
|
1255
|
+
value: () => this.pagination({ ...options, position: position + limit }),
|
|
1256
|
+
enumerable: false,
|
|
1257
|
+
configurable: false
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
else {
|
|
1261
|
+
pagination.hasMore = false;
|
|
1262
|
+
}
|
|
1263
|
+
if (hasPrev) {
|
|
1264
|
+
pagination.hasPrev = true;
|
|
1265
|
+
pagination.prevPos = Math.max(0, position - limit);
|
|
1266
|
+
Object.defineProperty(pagination, 'loadPrev', {
|
|
1267
|
+
value: () => this.pagination({ ...options, position: Math.max(0, position - limit) }),
|
|
1268
|
+
enumerable: false,
|
|
1269
|
+
configurable: false
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
else {
|
|
1273
|
+
pagination.hasPrev = false;
|
|
1274
|
+
}
|
|
1275
|
+
const getVisibleProps = () => {
|
|
1276
|
+
const visible = { position, limit };
|
|
1277
|
+
if (shouldCount)
|
|
1278
|
+
visible.total = total;
|
|
1279
|
+
visible.hasMore = hasMore;
|
|
1280
|
+
if (hasMore)
|
|
1281
|
+
visible.nextPos = position + limit;
|
|
1282
|
+
visible.hasPrev = hasPrev;
|
|
1283
|
+
if (hasPrev)
|
|
1284
|
+
visible.prevPos = Math.max(0, position - limit);
|
|
1285
|
+
return visible;
|
|
1286
|
+
};
|
|
1287
|
+
Object.defineProperty(pagination, Symbol.for('nodejs.util.inspect.custom'), {
|
|
1288
|
+
value: () => getVisibleProps(),
|
|
1289
|
+
enumerable: false
|
|
1290
|
+
});
|
|
1291
|
+
Object.defineProperty(pagination, 'toJSON', {
|
|
1292
|
+
value: () => getVisibleProps(),
|
|
1293
|
+
enumerable: false
|
|
1294
|
+
});
|
|
1295
|
+
return { data, pagination };
|
|
1296
|
+
}
|
|
1297
|
+
throw new RelqQueryError('Invalid pagination mode');
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
class PaginateBuilder {
|
|
1301
|
+
relq;
|
|
1302
|
+
tableName;
|
|
1303
|
+
columns;
|
|
1304
|
+
whereClause;
|
|
1305
|
+
orderByClause;
|
|
1306
|
+
constructor(relq, tableName, columns, whereClause, orderByClause) {
|
|
1307
|
+
this.relq = relq;
|
|
1308
|
+
this.tableName = tableName;
|
|
1309
|
+
this.columns = columns;
|
|
1310
|
+
this.whereClause = whereClause;
|
|
1311
|
+
this.orderByClause = orderByClause;
|
|
1312
|
+
}
|
|
1313
|
+
async paging(options) {
|
|
1314
|
+
const page = options.page ?? 1;
|
|
1315
|
+
const perPage = options.perPage;
|
|
1316
|
+
const shouldCount = options.count ?? true;
|
|
1317
|
+
if (page < 1) {
|
|
1318
|
+
throw new RelqQueryError('page must be >= 1', { hint: 'Page numbers are 1-indexed' });
|
|
1319
|
+
}
|
|
1320
|
+
if (perPage < 1) {
|
|
1321
|
+
throw new RelqQueryError('perPage must be >= 1');
|
|
1322
|
+
}
|
|
1323
|
+
const columnsToSelect = this.columns && this.columns.length > 0 ? this.columns : ['*'];
|
|
1324
|
+
const orderByArr = this.orderByClause
|
|
1325
|
+
? (Array.isArray(this.orderByClause[0]) ? this.orderByClause : [this.orderByClause])
|
|
1326
|
+
: [];
|
|
1327
|
+
const selectBuilder = new SelectBuilder(this.tableName, columnsToSelect);
|
|
1328
|
+
if (this.whereClause) {
|
|
1329
|
+
selectBuilder.where(this.whereClause);
|
|
1330
|
+
}
|
|
1331
|
+
for (const [column, direction] of orderByArr) {
|
|
1332
|
+
const dbColumn = this.relq[INTERNAL].transformToDbColumns(this.tableName, { [column]: true });
|
|
1333
|
+
selectBuilder.orderBy(Object.keys(dbColumn)[0] || column, direction);
|
|
1334
|
+
}
|
|
1335
|
+
let total = 0;
|
|
1336
|
+
if (shouldCount) {
|
|
1337
|
+
const countBuilder = new CountBuilder(this.tableName);
|
|
1338
|
+
if (this.whereClause) {
|
|
1339
|
+
countBuilder.where(this.whereClause);
|
|
1340
|
+
}
|
|
1341
|
+
const countResult = await this.relq[INTERNAL].executeCount(countBuilder.toString());
|
|
1342
|
+
total = countResult.count;
|
|
1343
|
+
}
|
|
1344
|
+
const offset = (page - 1) * perPage;
|
|
1345
|
+
selectBuilder.limit(perPage);
|
|
1346
|
+
selectBuilder.offset(offset);
|
|
1347
|
+
const result = await this.relq[INTERNAL].executeSelect(selectBuilder.toString(), this.tableName);
|
|
1348
|
+
const totalPages = Math.ceil(total / perPage);
|
|
1349
|
+
const hasNext = page < totalPages;
|
|
1350
|
+
const hasPrev = page > 1;
|
|
1351
|
+
const pagination = {
|
|
1352
|
+
page,
|
|
1353
|
+
perPage,
|
|
1354
|
+
total,
|
|
1355
|
+
totalPages,
|
|
1356
|
+
};
|
|
1357
|
+
if (hasNext) {
|
|
1358
|
+
pagination.hasNext = true;
|
|
1359
|
+
pagination.nextPage = page + 1;
|
|
1360
|
+
Object.defineProperty(pagination, 'loadNext', {
|
|
1361
|
+
value: () => this.paging({ ...options, page: page + 1 }),
|
|
1362
|
+
enumerable: false
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
else {
|
|
1366
|
+
pagination.hasNext = false;
|
|
1367
|
+
}
|
|
1368
|
+
if (hasPrev) {
|
|
1369
|
+
pagination.hasPrev = true;
|
|
1370
|
+
pagination.prevPage = page - 1;
|
|
1371
|
+
Object.defineProperty(pagination, 'loadPrev', {
|
|
1372
|
+
value: () => this.paging({ ...options, page: page - 1 }),
|
|
1373
|
+
enumerable: false
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
pagination.hasPrev = false;
|
|
1378
|
+
}
|
|
1379
|
+
Object.defineProperty(pagination, 'toJSON', {
|
|
1380
|
+
value: () => ({ page, perPage, total, totalPages, hasNext: pagination.hasNext, hasPrev: pagination.hasPrev, nextPage: pagination.nextPage, prevPage: pagination.prevPage }),
|
|
1381
|
+
enumerable: false
|
|
1382
|
+
});
|
|
1383
|
+
return { data: result.data, pagination };
|
|
1384
|
+
}
|
|
1385
|
+
async offset(options) {
|
|
1386
|
+
const position = options.position ?? 0;
|
|
1387
|
+
const shouldCount = options.count ?? false;
|
|
1388
|
+
if (position < 0) {
|
|
1389
|
+
throw new RelqQueryError('position must be >= 0');
|
|
1390
|
+
}
|
|
1391
|
+
let limit;
|
|
1392
|
+
if (options.shuffleLimit) {
|
|
1393
|
+
const [min, max] = options.shuffleLimit;
|
|
1394
|
+
if (min < 1 || max < 1) {
|
|
1395
|
+
throw new RelqQueryError('shuffleLimit values must be >= 1', { hint: 'Use [min, max] where both are >= 1' });
|
|
1396
|
+
}
|
|
1397
|
+
if (min > max) {
|
|
1398
|
+
throw new RelqQueryError('shuffleLimit[0] must be <= shuffleLimit[1]', { hint: 'Use [min, max] format where min <= max' });
|
|
1399
|
+
}
|
|
1400
|
+
limit = Math.floor(Math.random() * (max - min + 1)) + min;
|
|
1401
|
+
}
|
|
1402
|
+
else {
|
|
1403
|
+
const limitValue = options.limit;
|
|
1404
|
+
if (limitValue !== undefined && limitValue < 1) {
|
|
1405
|
+
throw new RelqQueryError('limit must be >= 1');
|
|
1406
|
+
}
|
|
1407
|
+
limit = limitValue ?? 50;
|
|
1408
|
+
}
|
|
1409
|
+
const columnsToSelect = this.columns && this.columns.length > 0 ? this.columns : ['*'];
|
|
1410
|
+
const orderByArr = this.orderByClause
|
|
1411
|
+
? (Array.isArray(this.orderByClause[0]) ? this.orderByClause : [this.orderByClause])
|
|
1412
|
+
: [];
|
|
1413
|
+
const selectBuilder = new SelectBuilder(this.tableName, columnsToSelect);
|
|
1414
|
+
if (this.whereClause) {
|
|
1415
|
+
selectBuilder.where(this.whereClause);
|
|
1416
|
+
}
|
|
1417
|
+
for (const [column, direction] of orderByArr) {
|
|
1418
|
+
const dbColumn = this.relq[INTERNAL].transformToDbColumns(this.tableName, { [column]: true });
|
|
1419
|
+
selectBuilder.orderBy(Object.keys(dbColumn)[0] || column, direction);
|
|
1420
|
+
}
|
|
1421
|
+
let total = 0;
|
|
1422
|
+
if (shouldCount) {
|
|
1423
|
+
const countBuilder = new CountBuilder(this.tableName);
|
|
1424
|
+
if (this.whereClause) {
|
|
1425
|
+
countBuilder.where(this.whereClause);
|
|
1426
|
+
}
|
|
1427
|
+
const countResult = await this.relq[INTERNAL].executeCount(countBuilder.toString());
|
|
1428
|
+
total = countResult.count;
|
|
1429
|
+
}
|
|
1430
|
+
selectBuilder.limit(limit + 1);
|
|
1431
|
+
selectBuilder.offset(position);
|
|
1432
|
+
const result = await this.relq[INTERNAL].executeSelect(selectBuilder.toString(), this.tableName);
|
|
1433
|
+
const hasMore = result.data.length > limit;
|
|
1434
|
+
const data = hasMore ? result.data.slice(0, limit) : result.data;
|
|
1435
|
+
const hasPrev = position > 0;
|
|
1436
|
+
const pagination = {
|
|
1437
|
+
position,
|
|
1438
|
+
limit,
|
|
1439
|
+
total,
|
|
1440
|
+
};
|
|
1441
|
+
if (hasMore) {
|
|
1442
|
+
pagination.hasMore = true;
|
|
1443
|
+
pagination.nextPos = position + limit;
|
|
1444
|
+
Object.defineProperty(pagination, 'loadNext', {
|
|
1445
|
+
value: () => this.offset({ ...options, position: position + limit }),
|
|
1446
|
+
enumerable: false
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
else {
|
|
1450
|
+
pagination.hasMore = false;
|
|
1451
|
+
}
|
|
1452
|
+
if (hasPrev) {
|
|
1453
|
+
pagination.hasPrev = true;
|
|
1454
|
+
pagination.prevPos = Math.max(0, position - limit);
|
|
1455
|
+
Object.defineProperty(pagination, 'loadPrev', {
|
|
1456
|
+
value: () => this.offset({ ...options, position: Math.max(0, position - limit) }),
|
|
1457
|
+
enumerable: false
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
else {
|
|
1461
|
+
pagination.hasPrev = false;
|
|
1462
|
+
}
|
|
1463
|
+
Object.defineProperty(pagination, 'toJSON', {
|
|
1464
|
+
value: () => ({ position, limit, total, hasMore: pagination.hasMore, hasPrev: pagination.hasPrev, nextPos: pagination.nextPos, prevPos: pagination.prevPos }),
|
|
1465
|
+
enumerable: false
|
|
1466
|
+
});
|
|
1467
|
+
return { data, pagination };
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
class ReturningExecutor {
|
|
1471
|
+
builder;
|
|
1472
|
+
relq;
|
|
1473
|
+
constructor(builder, relq) {
|
|
1474
|
+
this.builder = builder;
|
|
1475
|
+
this.relq = relq;
|
|
1476
|
+
}
|
|
1477
|
+
toString() {
|
|
1478
|
+
return this.builder.toString();
|
|
1479
|
+
}
|
|
1480
|
+
async run(withMetadata) {
|
|
1481
|
+
if (this.builder instanceof InsertBuilder) {
|
|
1482
|
+
for (const row of this.builder.insertData) {
|
|
1483
|
+
this.relq[INTERNAL].validateData(this.builder.tableName, row, 'insert');
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
else if (this.builder instanceof UpdateBuilder) {
|
|
1487
|
+
this.relq[INTERNAL].validateData(this.builder.tableName, this.builder.updateData, 'update');
|
|
1488
|
+
}
|
|
1489
|
+
const sql = this.builder.toString();
|
|
1490
|
+
const result = await this.relq[INTERNAL].executeSelect(sql);
|
|
1491
|
+
if (withMetadata) {
|
|
1492
|
+
return result;
|
|
1493
|
+
}
|
|
1494
|
+
return result.data;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
class ConnectedInsertBuilder {
|
|
1498
|
+
builder;
|
|
1499
|
+
relq;
|
|
1500
|
+
constructor(builder, relq) {
|
|
1501
|
+
this.builder = builder;
|
|
1502
|
+
this.relq = relq;
|
|
1503
|
+
}
|
|
1504
|
+
addRow(row) {
|
|
1505
|
+
this.builder.addRow(row);
|
|
1506
|
+
return this;
|
|
1507
|
+
}
|
|
1508
|
+
addRows(rows) {
|
|
1509
|
+
this.builder.addRows(rows);
|
|
1510
|
+
return this;
|
|
1511
|
+
}
|
|
1512
|
+
clear() {
|
|
1513
|
+
this.builder.clear();
|
|
1514
|
+
return this;
|
|
1515
|
+
}
|
|
1516
|
+
get total() {
|
|
1517
|
+
return this.builder.total;
|
|
1518
|
+
}
|
|
1519
|
+
onConflict(columns, callback) {
|
|
1520
|
+
const cols = Array.isArray(columns) ? columns : [columns];
|
|
1521
|
+
this.builder.onConflict(cols, (conflictBuilder) => {
|
|
1522
|
+
callback(conflictBuilder);
|
|
1523
|
+
});
|
|
1524
|
+
return this;
|
|
1525
|
+
}
|
|
1526
|
+
toString() {
|
|
1527
|
+
return this.builder.toString();
|
|
1528
|
+
}
|
|
1529
|
+
async run(withMetadata) {
|
|
1530
|
+
debugLog(this.relq[INTERNAL]?.config, `ConnectedInsertBuilder.run called for table: ${this.builder.tableName}`);
|
|
1531
|
+
const internalRelq = this.relq[INTERNAL];
|
|
1532
|
+
for (const row of this.builder.insertData) {
|
|
1533
|
+
internalRelq.validateData(this.builder.tableName, row, 'insert');
|
|
1534
|
+
}
|
|
1535
|
+
const sql = this.builder.toString();
|
|
1536
|
+
const result = await internalRelq.executeRun(sql);
|
|
1537
|
+
if (withMetadata) {
|
|
1538
|
+
return result;
|
|
1539
|
+
}
|
|
1540
|
+
return result.metadata.rowCount ?? 0;
|
|
1541
|
+
}
|
|
1542
|
+
returning(columns) {
|
|
1543
|
+
this.builder.returning(columns);
|
|
1544
|
+
return new ReturningExecutor(this.builder, this.relq);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
class ConnectedUpdateBuilder {
|
|
1548
|
+
builder;
|
|
1549
|
+
relq;
|
|
1550
|
+
constructor(builder, relq) {
|
|
1551
|
+
this.builder = builder;
|
|
1552
|
+
this.relq = relq;
|
|
1553
|
+
}
|
|
1554
|
+
where(callback) {
|
|
1555
|
+
this.builder.where(callback);
|
|
1556
|
+
return this;
|
|
1557
|
+
}
|
|
1558
|
+
toString() {
|
|
1559
|
+
return this.builder.toString();
|
|
1560
|
+
}
|
|
1561
|
+
async run(withMetadata) {
|
|
1562
|
+
this.relq[INTERNAL].validateData(this.builder.tableName, this.builder.updateData, 'update');
|
|
1563
|
+
const sql = this.builder.toString();
|
|
1564
|
+
const result = await this.relq[INTERNAL].executeRun(sql);
|
|
1565
|
+
if (withMetadata) {
|
|
1566
|
+
return result;
|
|
1567
|
+
}
|
|
1568
|
+
return result.metadata.rowCount ?? 0;
|
|
1569
|
+
}
|
|
1570
|
+
returning(columns) {
|
|
1571
|
+
this.builder.returning(columns);
|
|
1572
|
+
return new ReturningExecutor(this.builder, this.relq);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
class ConnectedDeleteBuilder {
|
|
1576
|
+
builder;
|
|
1577
|
+
relq;
|
|
1578
|
+
constructor(builder, relq) {
|
|
1579
|
+
this.builder = builder;
|
|
1580
|
+
this.relq = relq;
|
|
1581
|
+
}
|
|
1582
|
+
where(callback) {
|
|
1583
|
+
this.builder.where(callback);
|
|
1584
|
+
return this;
|
|
1585
|
+
}
|
|
1586
|
+
toString() {
|
|
1587
|
+
return this.builder.toString();
|
|
1588
|
+
}
|
|
1589
|
+
async run(withMetadata) {
|
|
1590
|
+
const sql = this.builder.toString();
|
|
1591
|
+
const result = await this.relq[INTERNAL].executeRun(sql);
|
|
1592
|
+
if (withMetadata) {
|
|
1593
|
+
return result;
|
|
1594
|
+
}
|
|
1595
|
+
return result.metadata.rowCount ?? 0;
|
|
1596
|
+
}
|
|
1597
|
+
returning(columns) {
|
|
1598
|
+
this.builder.returning(columns);
|
|
1599
|
+
return new ReturningExecutor(this.builder, this.relq);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
class ConnectedCountBuilder {
|
|
1603
|
+
builder;
|
|
1604
|
+
relq;
|
|
1605
|
+
tableName;
|
|
1606
|
+
groupNames = [];
|
|
1607
|
+
constructor(builder, relq, tableName) {
|
|
1608
|
+
this.builder = builder;
|
|
1609
|
+
this.relq = relq;
|
|
1610
|
+
this.tableName = tableName;
|
|
1611
|
+
}
|
|
1612
|
+
group(name, callback, options) {
|
|
1613
|
+
this.groupNames.push(name);
|
|
1614
|
+
this.builder.group(name, (q) => {
|
|
1615
|
+
const wrapped = this.wrapConditionBuilder(q);
|
|
1616
|
+
return callback(wrapped);
|
|
1617
|
+
}, options);
|
|
1618
|
+
return this;
|
|
1619
|
+
}
|
|
1620
|
+
where(callback) {
|
|
1621
|
+
this.builder.where((q) => {
|
|
1622
|
+
const wrapped = this.wrapConditionBuilder(q);
|
|
1623
|
+
return callback(wrapped);
|
|
1624
|
+
});
|
|
1625
|
+
return this;
|
|
1626
|
+
}
|
|
1627
|
+
wrapConditionBuilder(originalBuilder) {
|
|
1628
|
+
const relq = this.relq;
|
|
1629
|
+
const tableName = this.tableName;
|
|
1630
|
+
return new Proxy(originalBuilder, {
|
|
1631
|
+
get(target, prop) {
|
|
1632
|
+
const original = target[prop];
|
|
1633
|
+
if (typeof original === 'function') {
|
|
1634
|
+
return function (column, ...args) {
|
|
1635
|
+
const transformed = relq[INTERNAL].transformToDbColumns(tableName, { [column]: true });
|
|
1636
|
+
const dbColumn = Object.keys(transformed)[0] || column;
|
|
1637
|
+
return original.call(target, dbColumn, ...args);
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
return original;
|
|
1641
|
+
}
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
toString() {
|
|
1645
|
+
return this.builder.toString();
|
|
1646
|
+
}
|
|
1647
|
+
async execute() {
|
|
1648
|
+
const sql = this.builder.toString();
|
|
1649
|
+
return this.relq[INTERNAL].executeCount(sql);
|
|
1650
|
+
}
|
|
1651
|
+
async get() {
|
|
1652
|
+
const sql = this.builder.toString();
|
|
1653
|
+
const result = await this.relq[INTERNAL].executeQuery(sql);
|
|
1654
|
+
const row = result.result.rows[0];
|
|
1655
|
+
if (this.groupNames.length === 0) {
|
|
1656
|
+
return (Number(row?.count) ?? 0);
|
|
1657
|
+
}
|
|
1658
|
+
const counts = {};
|
|
1659
|
+
for (const name of this.groupNames) {
|
|
1660
|
+
counts[name] = Number(row?.[name] ?? 0);
|
|
1661
|
+
}
|
|
1662
|
+
return counts;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
class ConnectedRawQueryBuilder {
|
|
1666
|
+
query;
|
|
1667
|
+
params;
|
|
1668
|
+
relq;
|
|
1669
|
+
builder;
|
|
1670
|
+
constructor(query, params, relq) {
|
|
1671
|
+
this.query = query;
|
|
1672
|
+
this.params = params;
|
|
1673
|
+
this.relq = relq;
|
|
1674
|
+
const convertedQuery = this.convertPlaceholders(query);
|
|
1675
|
+
this.builder = new RawQueryBuilder(convertedQuery, this.params);
|
|
1676
|
+
}
|
|
1677
|
+
convertPlaceholders(query) {
|
|
1678
|
+
let index = 0;
|
|
1679
|
+
return query.replace(/\?/g, () => `$${++index}`);
|
|
1680
|
+
}
|
|
1681
|
+
async all() {
|
|
1682
|
+
const sql = this.builder.toString();
|
|
1683
|
+
return this.relq[INTERNAL].executeSelect(sql);
|
|
1684
|
+
}
|
|
1685
|
+
async get() {
|
|
1686
|
+
const sql = this.builder.toString();
|
|
1687
|
+
return this.relq[INTERNAL].executeSelectOne(sql);
|
|
1688
|
+
}
|
|
1689
|
+
async getMany(count) {
|
|
1690
|
+
const sql = this.builder.toString();
|
|
1691
|
+
const limitedSql = sql.toUpperCase().includes('LIMIT')
|
|
1692
|
+
? sql
|
|
1693
|
+
: `${sql} LIMIT ${count}`;
|
|
1694
|
+
return this.relq[INTERNAL].executeSelect(limitedSql);
|
|
1695
|
+
}
|
|
1696
|
+
async run() {
|
|
1697
|
+
const sql = this.builder.toString();
|
|
1698
|
+
return this.relq[INTERNAL].executeRun(sql);
|
|
1699
|
+
}
|
|
1700
|
+
async count() {
|
|
1701
|
+
const sql = this.builder.toString();
|
|
1702
|
+
return this.relq[INTERNAL].executeCount(sql);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
class ConnectedTransactionBuilder {
|
|
1706
|
+
relq;
|
|
1707
|
+
constructor(relq) {
|
|
1708
|
+
this.relq = relq;
|
|
1709
|
+
}
|
|
1710
|
+
builder = new TransactionBuilder();
|
|
1711
|
+
toString() {
|
|
1712
|
+
return this.builder.toString();
|
|
1713
|
+
}
|
|
1714
|
+
async begin() {
|
|
1715
|
+
const sql = this.builder.toString();
|
|
1716
|
+
return this.relq[INTERNAL].executeRun(sql);
|
|
1717
|
+
}
|
|
1718
|
+
async commit() {
|
|
1719
|
+
const sql = this.builder.commit();
|
|
1720
|
+
return this.relq[INTERNAL].executeRun(sql);
|
|
1721
|
+
}
|
|
1722
|
+
async rollback() {
|
|
1723
|
+
const sql = this.builder.rollback();
|
|
1724
|
+
return this.relq[INTERNAL].executeRun(sql);
|
|
1725
|
+
}
|
|
1726
|
+
isolationLevel(level) {
|
|
1727
|
+
this.builder.isolation(level);
|
|
1728
|
+
return this;
|
|
1729
|
+
}
|
|
1730
|
+
readOnly() {
|
|
1731
|
+
this.builder.readOnly();
|
|
1732
|
+
return this;
|
|
1733
|
+
}
|
|
1734
|
+
readWrite() {
|
|
1735
|
+
this.builder.readWrite();
|
|
1736
|
+
return this;
|
|
1737
|
+
}
|
|
1738
|
+
deferrable() {
|
|
1739
|
+
this.builder.deferrable();
|
|
1740
|
+
return this;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
class ConnectedCTEBuilder {
|
|
1744
|
+
relq;
|
|
1745
|
+
ctes = [];
|
|
1746
|
+
constructor(relq) {
|
|
1747
|
+
this.relq = relq;
|
|
1748
|
+
}
|
|
1749
|
+
with(name, query) {
|
|
1750
|
+
const queryStr = typeof query === 'string' ? query : query.toString();
|
|
1751
|
+
this.ctes.push({ name, query: queryStr });
|
|
1752
|
+
return this;
|
|
1753
|
+
}
|
|
1754
|
+
withMaterialized(name, query) {
|
|
1755
|
+
const queryStr = typeof query === 'string' ? query : query.toString();
|
|
1756
|
+
this.ctes.push({ name, query: `MATERIALIZED (${queryStr})` });
|
|
1757
|
+
return this;
|
|
1758
|
+
}
|
|
1759
|
+
async query(sql) {
|
|
1760
|
+
const cteClause = this.buildCTEClause();
|
|
1761
|
+
const fullSql = `${cteClause} ${sql}`;
|
|
1762
|
+
const result = await this.relq[INTERNAL].executeQuery(fullSql);
|
|
1763
|
+
return {
|
|
1764
|
+
data: result.result.rows,
|
|
1765
|
+
rowCount: result.result.rowCount ?? 0
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
async all(sql) {
|
|
1769
|
+
const result = await this.query(sql);
|
|
1770
|
+
return result.data;
|
|
1771
|
+
}
|
|
1772
|
+
async get(sql) {
|
|
1773
|
+
const result = await this.query(sql);
|
|
1774
|
+
return result.data[0] ?? null;
|
|
1775
|
+
}
|
|
1776
|
+
buildCTEClause() {
|
|
1777
|
+
if (this.ctes.length === 0)
|
|
1778
|
+
return '';
|
|
1779
|
+
const cteStrings = this.ctes.map(cte => {
|
|
1780
|
+
if (cte.query.startsWith('MATERIALIZED')) {
|
|
1781
|
+
return `"${cte.name}" AS ${cte.query}`;
|
|
1782
|
+
}
|
|
1783
|
+
return `"${cte.name}" AS (${cte.query})`;
|
|
1784
|
+
});
|
|
1785
|
+
return `WITH ${cteStrings.join(', ')}`;
|
|
1786
|
+
}
|
|
1787
|
+
toSQL(mainQuery) {
|
|
1788
|
+
return `${this.buildCTEClause()} ${mainQuery}`;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
export default Relq;
|