relq 1.0.42 → 1.0.44
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/dist/cjs/core/relq-client.cjs +9 -0
- package/dist/cjs/select/scalar-query-builder.cjs +134 -0
- package/dist/cjs/select/scalar-select-builder.cjs +78 -0
- package/dist/cjs/types/scalar-types.cjs +2 -0
- package/dist/cjs/utils/fk-resolver.cjs +20 -0
- package/dist/esm/core/relq-client.js +9 -0
- package/dist/esm/select/scalar-query-builder.js +126 -0
- package/dist/esm/select/scalar-select-builder.js +70 -0
- package/dist/esm/types/scalar-types.js +1 -0
- package/dist/esm/utils/fk-resolver.js +20 -0
- package/dist/index.d.ts +176 -2
- package/package.json +1 -1
|
@@ -43,6 +43,7 @@ const pool_defaults_1 = require("../utils/pool-defaults.cjs");
|
|
|
43
43
|
const relq_errors_1 = require("../errors/relq-errors.cjs");
|
|
44
44
|
const type_coercion_1 = require("../utils/type-coercion.cjs");
|
|
45
45
|
const helpers_1 = require("./helpers/index.cjs");
|
|
46
|
+
const scalar_select_builder_1 = require("../select/scalar-select-builder.cjs");
|
|
46
47
|
let PgPool = null;
|
|
47
48
|
let PgClient = null;
|
|
48
49
|
const activeRelqInstances = new Set();
|
|
@@ -720,6 +721,14 @@ class Relq {
|
|
|
720
721
|
}
|
|
721
722
|
return result.result.rows.map((r) => r['QUERY PLAN'] || Object.values(r)[0]).join('\n');
|
|
722
723
|
}
|
|
724
|
+
scalar(scalars) {
|
|
725
|
+
const schema = this._getSchema();
|
|
726
|
+
return new scalar_select_builder_1.ConnectedScalarSelectBuilder(scalars, schema, {
|
|
727
|
+
executeSelectOne: this[helpers_1.INTERNAL].executeSelect.bind(this),
|
|
728
|
+
hasColumnMapping: this._hasColumnMapping.bind(this),
|
|
729
|
+
transformToDbColumns: this._transformToDbColumns.bind(this)
|
|
730
|
+
});
|
|
731
|
+
}
|
|
723
732
|
}
|
|
724
733
|
exports.Relq = Relq;
|
|
725
734
|
exports.default = Relq;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ScalarQueryBuilderImpl = void 0;
|
|
7
|
+
exports.createScalarTableAccessor = createScalarTableAccessor;
|
|
8
|
+
const pg_format_1 = __importDefault(require("../addon/pg-format/index.cjs"));
|
|
9
|
+
const condition_collector_1 = require("../condition/condition-collector.cjs");
|
|
10
|
+
class ScalarResultImpl {
|
|
11
|
+
sql;
|
|
12
|
+
params;
|
|
13
|
+
$scalar = true;
|
|
14
|
+
$type;
|
|
15
|
+
constructor(sql, params = []) {
|
|
16
|
+
this.sql = sql;
|
|
17
|
+
this.params = params;
|
|
18
|
+
}
|
|
19
|
+
toSQL() {
|
|
20
|
+
return this.sql;
|
|
21
|
+
}
|
|
22
|
+
getParams() {
|
|
23
|
+
return this.params;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
class ScalarQueryBuilderImpl {
|
|
27
|
+
tableName;
|
|
28
|
+
tableSchema;
|
|
29
|
+
columnResolver;
|
|
30
|
+
whereConditions = [];
|
|
31
|
+
constructor(tableName, tableSchema, columnResolver) {
|
|
32
|
+
this.tableName = tableName;
|
|
33
|
+
this.tableSchema = tableSchema;
|
|
34
|
+
this.columnResolver = columnResolver;
|
|
35
|
+
}
|
|
36
|
+
where(callback) {
|
|
37
|
+
const conditionBuilder = new condition_collector_1.ConditionCollector();
|
|
38
|
+
callback(conditionBuilder);
|
|
39
|
+
this.whereConditions.push(...conditionBuilder.getConditions());
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
count() {
|
|
43
|
+
const sql = this.buildSQL('COUNT(*)');
|
|
44
|
+
return new ScalarResultImpl(sql);
|
|
45
|
+
}
|
|
46
|
+
sum(column) {
|
|
47
|
+
const sqlColumn = this.resolveColumn(column);
|
|
48
|
+
const sql = this.buildSQL(`COALESCE(SUM(${pg_format_1.default.ident(sqlColumn)}), 0)`);
|
|
49
|
+
return new ScalarResultImpl(sql);
|
|
50
|
+
}
|
|
51
|
+
avg(column) {
|
|
52
|
+
const sqlColumn = this.resolveColumn(column);
|
|
53
|
+
const sql = this.buildSQL(`COALESCE(AVG(${pg_format_1.default.ident(sqlColumn)}), 0)`);
|
|
54
|
+
return new ScalarResultImpl(sql);
|
|
55
|
+
}
|
|
56
|
+
min(column) {
|
|
57
|
+
const sqlColumn = this.resolveColumn(column);
|
|
58
|
+
const sql = this.buildSQL(`MIN(${pg_format_1.default.ident(sqlColumn)})`);
|
|
59
|
+
return new ScalarResultImpl(sql);
|
|
60
|
+
}
|
|
61
|
+
max(column) {
|
|
62
|
+
const sqlColumn = this.resolveColumn(column);
|
|
63
|
+
const sql = this.buildSQL(`MAX(${pg_format_1.default.ident(sqlColumn)})`);
|
|
64
|
+
return new ScalarResultImpl(sql);
|
|
65
|
+
}
|
|
66
|
+
pick(column) {
|
|
67
|
+
const sqlColumn = this.resolveColumn(column);
|
|
68
|
+
const sql = this.buildSQL(pg_format_1.default.ident(sqlColumn), true);
|
|
69
|
+
return new ScalarResultImpl(sql);
|
|
70
|
+
}
|
|
71
|
+
exists() {
|
|
72
|
+
const whereClause = this.buildWhereClause();
|
|
73
|
+
const sql = `(EXISTS(SELECT 1 FROM ${pg_format_1.default.ident(this.tableName)}${whereClause}))`;
|
|
74
|
+
return new ScalarResultImpl(sql);
|
|
75
|
+
}
|
|
76
|
+
resolveColumn(column) {
|
|
77
|
+
if (this.columnResolver) {
|
|
78
|
+
return this.columnResolver(column);
|
|
79
|
+
}
|
|
80
|
+
const columns = this.tableSchema?.$columns || this.tableSchema;
|
|
81
|
+
const columnDef = columns?.[column];
|
|
82
|
+
if (columnDef?.$columnName) {
|
|
83
|
+
return columnDef.$columnName;
|
|
84
|
+
}
|
|
85
|
+
return column.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
|
|
86
|
+
}
|
|
87
|
+
transformConditions(conditions) {
|
|
88
|
+
if (!this.columnResolver && !this.tableSchema) {
|
|
89
|
+
return conditions;
|
|
90
|
+
}
|
|
91
|
+
return conditions.map(cond => {
|
|
92
|
+
if (!cond.column || cond.method === 'raw') {
|
|
93
|
+
return cond;
|
|
94
|
+
}
|
|
95
|
+
if (cond.method === 'or' || cond.method === 'and') {
|
|
96
|
+
return {
|
|
97
|
+
...cond,
|
|
98
|
+
values: this.transformConditions(cond.values)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
...cond,
|
|
103
|
+
column: this.resolveColumn(cond.column)
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
buildWhereClause() {
|
|
108
|
+
if (this.whereConditions.length === 0) {
|
|
109
|
+
return '';
|
|
110
|
+
}
|
|
111
|
+
const transformed = this.transformConditions(this.whereConditions);
|
|
112
|
+
return ` WHERE ${(0, condition_collector_1.buildConditionsSQL)(transformed)}`;
|
|
113
|
+
}
|
|
114
|
+
buildSQL(selectExpr, withLimit = false) {
|
|
115
|
+
const whereClause = this.buildWhereClause();
|
|
116
|
+
const limitClause = withLimit ? ' LIMIT 1' : '';
|
|
117
|
+
return `(SELECT ${selectExpr} FROM ${pg_format_1.default.ident(this.tableName)}${whereClause}${limitClause})`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
exports.ScalarQueryBuilderImpl = ScalarQueryBuilderImpl;
|
|
121
|
+
function createScalarTableAccessor(schema, getColumnResolver) {
|
|
122
|
+
return new Proxy({}, {
|
|
123
|
+
get(_, prop) {
|
|
124
|
+
if (typeof prop === 'symbol') {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const tables = schema?.tables || schema;
|
|
128
|
+
const tableDef = tables?.[prop];
|
|
129
|
+
const sqlTableName = tableDef?.$name || prop;
|
|
130
|
+
const columnResolver = getColumnResolver?.(sqlTableName);
|
|
131
|
+
return new ScalarQueryBuilderImpl(sqlTableName, tableDef, columnResolver);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ConnectedScalarSelectBuilder = exports.ScalarSelectBuilder = void 0;
|
|
7
|
+
const pg_format_1 = __importDefault(require("../addon/pg-format/index.cjs"));
|
|
8
|
+
const scalar_query_builder_1 = require("./scalar-query-builder.cjs");
|
|
9
|
+
class ScalarSelectBuilder {
|
|
10
|
+
executeQuery;
|
|
11
|
+
scalarResults;
|
|
12
|
+
constructor(scalars, schema, executeQuery, getColumnResolver) {
|
|
13
|
+
this.executeQuery = executeQuery;
|
|
14
|
+
const tableAccessor = (0, scalar_query_builder_1.createScalarTableAccessor)(schema, getColumnResolver);
|
|
15
|
+
this.scalarResults = {};
|
|
16
|
+
for (const [key, callback] of Object.entries(scalars)) {
|
|
17
|
+
this.scalarResults[key] = callback(tableAccessor);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
toString() {
|
|
21
|
+
const columns = Object.entries(this.scalarResults)
|
|
22
|
+
.map(([alias, scalar]) => `${scalar.toSQL()} AS ${pg_format_1.default.ident(alias)}`)
|
|
23
|
+
.join(', ');
|
|
24
|
+
return `SELECT ${columns}`;
|
|
25
|
+
}
|
|
26
|
+
async get(withMetadata) {
|
|
27
|
+
const sql = this.toString();
|
|
28
|
+
const { data, metadata } = await this.executeQuery(sql);
|
|
29
|
+
const row = Array.isArray(data) ? data[0] : data;
|
|
30
|
+
const result = this.transformResult(row);
|
|
31
|
+
if (withMetadata) {
|
|
32
|
+
return { data: result, metadata };
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
transformResult(row) {
|
|
37
|
+
if (!row) {
|
|
38
|
+
const result = {};
|
|
39
|
+
for (const key of Object.keys(this.scalarResults)) {
|
|
40
|
+
result[key] = null;
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
const result = {};
|
|
45
|
+
for (const [key, value] of Object.entries(row)) {
|
|
46
|
+
if (typeof value === 'string' && /^\d+$/.test(value)) {
|
|
47
|
+
result[key] = parseInt(value, 10);
|
|
48
|
+
}
|
|
49
|
+
else if (typeof value === 'string' && /^\d+\.\d+$/.test(value)) {
|
|
50
|
+
result[key] = parseFloat(value);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
result[key] = value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.ScalarSelectBuilder = ScalarSelectBuilder;
|
|
60
|
+
class ConnectedScalarSelectBuilder extends ScalarSelectBuilder {
|
|
61
|
+
constructor(scalars, schema, internal) {
|
|
62
|
+
const executeQuery = async (sql) => {
|
|
63
|
+
return internal.executeSelectOne(sql);
|
|
64
|
+
};
|
|
65
|
+
const getColumnResolver = (tableName) => {
|
|
66
|
+
if (internal.hasColumnMapping()) {
|
|
67
|
+
return (column) => {
|
|
68
|
+
const transformed = internal.transformToDbColumns(tableName, { [column]: null });
|
|
69
|
+
const keys = Object.keys(transformed);
|
|
70
|
+
return keys.length > 0 ? keys[0] : column;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
};
|
|
75
|
+
super(scalars, schema, executeQuery, getColumnResolver);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.ConnectedScalarSelectBuilder = ConnectedScalarSelectBuilder;
|
|
@@ -16,6 +16,26 @@ class ForeignKeyResolutionError extends Error {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
exports.ForeignKeyResolutionError = ForeignKeyResolutionError;
|
|
19
|
+
function isArrayFormat(tableRelations) {
|
|
20
|
+
return Array.isArray(tableRelations);
|
|
21
|
+
}
|
|
22
|
+
function* iterateForeignKeys(tableRelations) {
|
|
23
|
+
if (!tableRelations)
|
|
24
|
+
return;
|
|
25
|
+
if (isArrayFormat(tableRelations)) {
|
|
26
|
+
for (const relationDef of tableRelations) {
|
|
27
|
+
if (relationDef.$type === 'foreignKey' && relationDef.$columns?.length > 0) {
|
|
28
|
+
const columnKey = relationDef.$columns[0].column;
|
|
29
|
+
yield [columnKey, relationDef];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
for (const [columnKey, relationDef] of Object.entries(tableRelations)) {
|
|
35
|
+
yield [columnKey, relationDef];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
19
39
|
function resolveForeignKey(relations, schema, fromTableKey, toTableKey) {
|
|
20
40
|
if (!relations) {
|
|
21
41
|
return null;
|
|
@@ -7,6 +7,7 @@ import { validatePoolConfig, formatPoolConfig, getSmartPoolDefaults } from "../u
|
|
|
7
7
|
import { RelqEnvironmentError, RelqConfigError, RelqQueryError, RelqConnectionError, parsePostgresError } from "../errors/relq-errors.js";
|
|
8
8
|
import { deserializeValue, serializeValue } from "../utils/type-coercion.js";
|
|
9
9
|
import { ConnectedCTEBuilder, ConnectedQueryBuilder, ConnectedRawQueryBuilder, ConnectedTransactionBuilder, INTERNAL, debugLog } from "./helpers/index.js";
|
|
10
|
+
import { ConnectedScalarSelectBuilder } from "../select/scalar-select-builder.js";
|
|
10
11
|
let PgPool = null;
|
|
11
12
|
let PgClient = null;
|
|
12
13
|
const activeRelqInstances = new Set();
|
|
@@ -684,5 +685,13 @@ export class Relq {
|
|
|
684
685
|
}
|
|
685
686
|
return result.result.rows.map((r) => r['QUERY PLAN'] || Object.values(r)[0]).join('\n');
|
|
686
687
|
}
|
|
688
|
+
scalar(scalars) {
|
|
689
|
+
const schema = this._getSchema();
|
|
690
|
+
return new ConnectedScalarSelectBuilder(scalars, schema, {
|
|
691
|
+
executeSelectOne: this[INTERNAL].executeSelect.bind(this),
|
|
692
|
+
hasColumnMapping: this._hasColumnMapping.bind(this),
|
|
693
|
+
transformToDbColumns: this._transformToDbColumns.bind(this)
|
|
694
|
+
});
|
|
695
|
+
}
|
|
687
696
|
}
|
|
688
697
|
export default Relq;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import format from "../addon/pg-format/index.js";
|
|
2
|
+
import { ConditionCollector, buildConditionsSQL } from "../condition/condition-collector.js";
|
|
3
|
+
class ScalarResultImpl {
|
|
4
|
+
sql;
|
|
5
|
+
params;
|
|
6
|
+
$scalar = true;
|
|
7
|
+
$type;
|
|
8
|
+
constructor(sql, params = []) {
|
|
9
|
+
this.sql = sql;
|
|
10
|
+
this.params = params;
|
|
11
|
+
}
|
|
12
|
+
toSQL() {
|
|
13
|
+
return this.sql;
|
|
14
|
+
}
|
|
15
|
+
getParams() {
|
|
16
|
+
return this.params;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class ScalarQueryBuilderImpl {
|
|
20
|
+
tableName;
|
|
21
|
+
tableSchema;
|
|
22
|
+
columnResolver;
|
|
23
|
+
whereConditions = [];
|
|
24
|
+
constructor(tableName, tableSchema, columnResolver) {
|
|
25
|
+
this.tableName = tableName;
|
|
26
|
+
this.tableSchema = tableSchema;
|
|
27
|
+
this.columnResolver = columnResolver;
|
|
28
|
+
}
|
|
29
|
+
where(callback) {
|
|
30
|
+
const conditionBuilder = new ConditionCollector();
|
|
31
|
+
callback(conditionBuilder);
|
|
32
|
+
this.whereConditions.push(...conditionBuilder.getConditions());
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
count() {
|
|
36
|
+
const sql = this.buildSQL('COUNT(*)');
|
|
37
|
+
return new ScalarResultImpl(sql);
|
|
38
|
+
}
|
|
39
|
+
sum(column) {
|
|
40
|
+
const sqlColumn = this.resolveColumn(column);
|
|
41
|
+
const sql = this.buildSQL(`COALESCE(SUM(${format.ident(sqlColumn)}), 0)`);
|
|
42
|
+
return new ScalarResultImpl(sql);
|
|
43
|
+
}
|
|
44
|
+
avg(column) {
|
|
45
|
+
const sqlColumn = this.resolveColumn(column);
|
|
46
|
+
const sql = this.buildSQL(`COALESCE(AVG(${format.ident(sqlColumn)}), 0)`);
|
|
47
|
+
return new ScalarResultImpl(sql);
|
|
48
|
+
}
|
|
49
|
+
min(column) {
|
|
50
|
+
const sqlColumn = this.resolveColumn(column);
|
|
51
|
+
const sql = this.buildSQL(`MIN(${format.ident(sqlColumn)})`);
|
|
52
|
+
return new ScalarResultImpl(sql);
|
|
53
|
+
}
|
|
54
|
+
max(column) {
|
|
55
|
+
const sqlColumn = this.resolveColumn(column);
|
|
56
|
+
const sql = this.buildSQL(`MAX(${format.ident(sqlColumn)})`);
|
|
57
|
+
return new ScalarResultImpl(sql);
|
|
58
|
+
}
|
|
59
|
+
pick(column) {
|
|
60
|
+
const sqlColumn = this.resolveColumn(column);
|
|
61
|
+
const sql = this.buildSQL(format.ident(sqlColumn), true);
|
|
62
|
+
return new ScalarResultImpl(sql);
|
|
63
|
+
}
|
|
64
|
+
exists() {
|
|
65
|
+
const whereClause = this.buildWhereClause();
|
|
66
|
+
const sql = `(EXISTS(SELECT 1 FROM ${format.ident(this.tableName)}${whereClause}))`;
|
|
67
|
+
return new ScalarResultImpl(sql);
|
|
68
|
+
}
|
|
69
|
+
resolveColumn(column) {
|
|
70
|
+
if (this.columnResolver) {
|
|
71
|
+
return this.columnResolver(column);
|
|
72
|
+
}
|
|
73
|
+
const columns = this.tableSchema?.$columns || this.tableSchema;
|
|
74
|
+
const columnDef = columns?.[column];
|
|
75
|
+
if (columnDef?.$columnName) {
|
|
76
|
+
return columnDef.$columnName;
|
|
77
|
+
}
|
|
78
|
+
return column.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
|
|
79
|
+
}
|
|
80
|
+
transformConditions(conditions) {
|
|
81
|
+
if (!this.columnResolver && !this.tableSchema) {
|
|
82
|
+
return conditions;
|
|
83
|
+
}
|
|
84
|
+
return conditions.map(cond => {
|
|
85
|
+
if (!cond.column || cond.method === 'raw') {
|
|
86
|
+
return cond;
|
|
87
|
+
}
|
|
88
|
+
if (cond.method === 'or' || cond.method === 'and') {
|
|
89
|
+
return {
|
|
90
|
+
...cond,
|
|
91
|
+
values: this.transformConditions(cond.values)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
...cond,
|
|
96
|
+
column: this.resolveColumn(cond.column)
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
buildWhereClause() {
|
|
101
|
+
if (this.whereConditions.length === 0) {
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
const transformed = this.transformConditions(this.whereConditions);
|
|
105
|
+
return ` WHERE ${buildConditionsSQL(transformed)}`;
|
|
106
|
+
}
|
|
107
|
+
buildSQL(selectExpr, withLimit = false) {
|
|
108
|
+
const whereClause = this.buildWhereClause();
|
|
109
|
+
const limitClause = withLimit ? ' LIMIT 1' : '';
|
|
110
|
+
return `(SELECT ${selectExpr} FROM ${format.ident(this.tableName)}${whereClause}${limitClause})`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function createScalarTableAccessor(schema, getColumnResolver) {
|
|
114
|
+
return new Proxy({}, {
|
|
115
|
+
get(_, prop) {
|
|
116
|
+
if (typeof prop === 'symbol') {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
const tables = schema?.tables || schema;
|
|
120
|
+
const tableDef = tables?.[prop];
|
|
121
|
+
const sqlTableName = tableDef?.$name || prop;
|
|
122
|
+
const columnResolver = getColumnResolver?.(sqlTableName);
|
|
123
|
+
return new ScalarQueryBuilderImpl(sqlTableName, tableDef, columnResolver);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import format from "../addon/pg-format/index.js";
|
|
2
|
+
import { createScalarTableAccessor } from "./scalar-query-builder.js";
|
|
3
|
+
export class ScalarSelectBuilder {
|
|
4
|
+
executeQuery;
|
|
5
|
+
scalarResults;
|
|
6
|
+
constructor(scalars, schema, executeQuery, getColumnResolver) {
|
|
7
|
+
this.executeQuery = executeQuery;
|
|
8
|
+
const tableAccessor = createScalarTableAccessor(schema, getColumnResolver);
|
|
9
|
+
this.scalarResults = {};
|
|
10
|
+
for (const [key, callback] of Object.entries(scalars)) {
|
|
11
|
+
this.scalarResults[key] = callback(tableAccessor);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
toString() {
|
|
15
|
+
const columns = Object.entries(this.scalarResults)
|
|
16
|
+
.map(([alias, scalar]) => `${scalar.toSQL()} AS ${format.ident(alias)}`)
|
|
17
|
+
.join(', ');
|
|
18
|
+
return `SELECT ${columns}`;
|
|
19
|
+
}
|
|
20
|
+
async get(withMetadata) {
|
|
21
|
+
const sql = this.toString();
|
|
22
|
+
const { data, metadata } = await this.executeQuery(sql);
|
|
23
|
+
const row = Array.isArray(data) ? data[0] : data;
|
|
24
|
+
const result = this.transformResult(row);
|
|
25
|
+
if (withMetadata) {
|
|
26
|
+
return { data: result, metadata };
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
transformResult(row) {
|
|
31
|
+
if (!row) {
|
|
32
|
+
const result = {};
|
|
33
|
+
for (const key of Object.keys(this.scalarResults)) {
|
|
34
|
+
result[key] = null;
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
const result = {};
|
|
39
|
+
for (const [key, value] of Object.entries(row)) {
|
|
40
|
+
if (typeof value === 'string' && /^\d+$/.test(value)) {
|
|
41
|
+
result[key] = parseInt(value, 10);
|
|
42
|
+
}
|
|
43
|
+
else if (typeof value === 'string' && /^\d+\.\d+$/.test(value)) {
|
|
44
|
+
result[key] = parseFloat(value);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
result[key] = value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export class ConnectedScalarSelectBuilder extends ScalarSelectBuilder {
|
|
54
|
+
constructor(scalars, schema, internal) {
|
|
55
|
+
const executeQuery = async (sql) => {
|
|
56
|
+
return internal.executeSelectOne(sql);
|
|
57
|
+
};
|
|
58
|
+
const getColumnResolver = (tableName) => {
|
|
59
|
+
if (internal.hasColumnMapping()) {
|
|
60
|
+
return (column) => {
|
|
61
|
+
const transformed = internal.transformToDbColumns(tableName, { [column]: null });
|
|
62
|
+
const keys = Object.keys(transformed);
|
|
63
|
+
return keys.length > 0 ? keys[0] : column;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
};
|
|
68
|
+
super(scalars, schema, executeQuery, getColumnResolver);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -8,6 +8,26 @@ export class ForeignKeyResolutionError extends Error {
|
|
|
8
8
|
this.name = 'ForeignKeyResolutionError';
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
+
function isArrayFormat(tableRelations) {
|
|
12
|
+
return Array.isArray(tableRelations);
|
|
13
|
+
}
|
|
14
|
+
function* iterateForeignKeys(tableRelations) {
|
|
15
|
+
if (!tableRelations)
|
|
16
|
+
return;
|
|
17
|
+
if (isArrayFormat(tableRelations)) {
|
|
18
|
+
for (const relationDef of tableRelations) {
|
|
19
|
+
if (relationDef.$type === 'foreignKey' && relationDef.$columns?.length > 0) {
|
|
20
|
+
const columnKey = relationDef.$columns[0].column;
|
|
21
|
+
yield [columnKey, relationDef];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
for (const [columnKey, relationDef] of Object.entries(tableRelations)) {
|
|
27
|
+
yield [columnKey, relationDef];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
11
31
|
export function resolveForeignKey(relations, schema, fromTableKey, toTableKey) {
|
|
12
32
|
if (!relations) {
|
|
13
33
|
return null;
|
package/dist/index.d.ts
CHANGED
|
@@ -5424,8 +5424,12 @@ export interface ForeignKeyRelationDef {
|
|
|
5424
5424
|
}
|
|
5425
5425
|
/** Relation definition type */
|
|
5426
5426
|
export type RelationDef = ForeignKeyRelationDef;
|
|
5427
|
+
/** Table's relations - NEW: array of FK definitions */
|
|
5428
|
+
export type TableRelationsArray = ForeignKeyRelationDef[];
|
|
5427
5429
|
/** Table's relations map (legacy format) */
|
|
5428
5430
|
export type TableRelations = Record<string, RelationDef>;
|
|
5431
|
+
/** All relations for a schema - NEW: array per table */
|
|
5432
|
+
export type SchemaRelationsArray = Record<string, TableRelationsArray>;
|
|
5429
5433
|
/** All relations for a schema (legacy) */
|
|
5430
5434
|
export type SchemaRelations = Record<string, TableRelations>;
|
|
5431
5435
|
/**
|
|
@@ -5735,7 +5739,7 @@ export interface RelqConfig {
|
|
|
5735
5739
|
* )
|
|
5736
5740
|
* ```
|
|
5737
5741
|
*/
|
|
5738
|
-
relations?: SchemaRelations;
|
|
5742
|
+
relations?: SchemaRelations | SchemaRelationsArray;
|
|
5739
5743
|
}
|
|
5740
5744
|
/**
|
|
5741
5745
|
* Unsubscribe function returned by `db.subscribe()`
|
|
@@ -7127,7 +7131,7 @@ export interface RelqInternal {
|
|
|
7127
7131
|
/** Get the schema object for type-safe joins */
|
|
7128
7132
|
getSchema(): Record<string, any> | undefined;
|
|
7129
7133
|
/** Get the relations object for FK auto-resolution */
|
|
7130
|
-
getRelations(): SchemaRelations | undefined;
|
|
7134
|
+
getRelations(): SchemaRelations | SchemaRelationsArray | undefined;
|
|
7131
7135
|
/** Get table definition by schema key */
|
|
7132
7136
|
getTableDef(tableKey: string): TableDefinition<any> | undefined;
|
|
7133
7137
|
}
|
|
@@ -7780,6 +7784,156 @@ declare class ConnectedRawQueryBuilder {
|
|
|
7780
7784
|
count(): Promise<RelqCount>;
|
|
7781
7785
|
}
|
|
7782
7786
|
declare const INTERNAL: unique symbol;
|
|
7787
|
+
/**
|
|
7788
|
+
* Marker interface for scalar subquery results.
|
|
7789
|
+
* Contains type information and SQL generation capability.
|
|
7790
|
+
*/
|
|
7791
|
+
export interface ScalarResult<T> {
|
|
7792
|
+
/** Marker to identify scalar results */
|
|
7793
|
+
readonly $scalar: true;
|
|
7794
|
+
/** Type marker for TypeScript inference (not used at runtime) */
|
|
7795
|
+
readonly $type: T;
|
|
7796
|
+
/** Generate SQL for this scalar subquery */
|
|
7797
|
+
toSQL(): string;
|
|
7798
|
+
/** Get bound parameters for this subquery */
|
|
7799
|
+
getParams(): unknown[];
|
|
7800
|
+
}
|
|
7801
|
+
/**
|
|
7802
|
+
* ScalarQueryBuilder interface for building scalar subqueries.
|
|
7803
|
+
* Provides filtering and terminal aggregate methods.
|
|
7804
|
+
*/
|
|
7805
|
+
export interface ScalarQueryBuilder<TTable> {
|
|
7806
|
+
/**
|
|
7807
|
+
* Add WHERE conditions to the scalar subquery
|
|
7808
|
+
*/
|
|
7809
|
+
where(callback: (q: TypedWhereCondition<TTable>) => any): this;
|
|
7810
|
+
/**
|
|
7811
|
+
* Count rows matching the conditions
|
|
7812
|
+
* @returns ScalarResult<number>
|
|
7813
|
+
*/
|
|
7814
|
+
count(): ScalarResult<number>;
|
|
7815
|
+
/**
|
|
7816
|
+
* Sum a numeric column
|
|
7817
|
+
* @param column - Column to sum (must be numeric)
|
|
7818
|
+
* @returns ScalarResult<number>
|
|
7819
|
+
*/
|
|
7820
|
+
sum<K extends ColumnName<TTable>>(column: K): ScalarResult<number>;
|
|
7821
|
+
/**
|
|
7822
|
+
* Average of a numeric column
|
|
7823
|
+
* @param column - Column to average (must be numeric)
|
|
7824
|
+
* @returns ScalarResult<number>
|
|
7825
|
+
*/
|
|
7826
|
+
avg<K extends ColumnName<TTable>>(column: K): ScalarResult<number>;
|
|
7827
|
+
/**
|
|
7828
|
+
* Minimum value of a column
|
|
7829
|
+
* @param column - Column to get minimum
|
|
7830
|
+
* @returns ScalarResult of the column's type
|
|
7831
|
+
*/
|
|
7832
|
+
min<K extends ColumnName<TTable>>(column: K): ScalarResult<TTable extends {
|
|
7833
|
+
$inferSelect: infer S;
|
|
7834
|
+
} ? K extends keyof S ? S[K] : any : any>;
|
|
7835
|
+
/**
|
|
7836
|
+
* Maximum value of a column
|
|
7837
|
+
* @param column - Column to get maximum
|
|
7838
|
+
* @returns ScalarResult of the column's type
|
|
7839
|
+
*/
|
|
7840
|
+
max<K extends ColumnName<TTable>>(column: K): ScalarResult<TTable extends {
|
|
7841
|
+
$inferSelect: infer S;
|
|
7842
|
+
} ? K extends keyof S ? S[K] : any : any>;
|
|
7843
|
+
/**
|
|
7844
|
+
* Pick a single value from the first row
|
|
7845
|
+
* @param column - Column to pick
|
|
7846
|
+
* @returns ScalarResult of the column's type (nullable)
|
|
7847
|
+
*/
|
|
7848
|
+
pick<K extends ColumnName<TTable>>(column: K): ScalarResult<(TTable extends {
|
|
7849
|
+
$inferSelect: infer S;
|
|
7850
|
+
} ? K extends keyof S ? S[K] : any : any) | null>;
|
|
7851
|
+
/**
|
|
7852
|
+
* Check if any rows exist
|
|
7853
|
+
* @returns ScalarResult<boolean>
|
|
7854
|
+
*/
|
|
7855
|
+
exists(): ScalarResult<boolean>;
|
|
7856
|
+
}
|
|
7857
|
+
/**
|
|
7858
|
+
* ScalarTableAccessor provides access to tables for scalar subqueries.
|
|
7859
|
+
* Each property is a ScalarQueryBuilder for that table.
|
|
7860
|
+
*
|
|
7861
|
+
* @example
|
|
7862
|
+
* ```typescript
|
|
7863
|
+
* // In scalar callback:
|
|
7864
|
+
* q => q.orders.where(w => w.equal('status', 'active')).count()
|
|
7865
|
+
* // ^^^^^^
|
|
7866
|
+
* // This is ScalarTableAccessor providing .orders
|
|
7867
|
+
* ```
|
|
7868
|
+
*/
|
|
7869
|
+
export type ScalarTableAccessor<TSchema> = {
|
|
7870
|
+
readonly [K in keyof TSchema & string]: ScalarQueryBuilder<TSchema[K]>;
|
|
7871
|
+
};
|
|
7872
|
+
/**
|
|
7873
|
+
* Callback function type for scalar definitions.
|
|
7874
|
+
* Takes a table accessor and returns a ScalarResult.
|
|
7875
|
+
*/
|
|
7876
|
+
export type ScalarCallback<TSchema, TResult> = (tables: ScalarTableAccessor<TSchema>) => ScalarResult<TResult>;
|
|
7877
|
+
/**
|
|
7878
|
+
* Infer result type from an object of scalar callbacks.
|
|
7879
|
+
*
|
|
7880
|
+
* @example
|
|
7881
|
+
* ```typescript
|
|
7882
|
+
* type Input = {
|
|
7883
|
+
* count: (q: ScalarTableAccessor<Schema>) => ScalarResult<number>;
|
|
7884
|
+
* name: (q: ScalarTableAccessor<Schema>) => ScalarResult<string | null>;
|
|
7885
|
+
* };
|
|
7886
|
+
* type Result = InferScalars<Input>;
|
|
7887
|
+
* // { count: number; name: string | null }
|
|
7888
|
+
* ```
|
|
7889
|
+
*/
|
|
7890
|
+
export type InferScalars<T extends Record<string, ScalarCallback<any, any>>> = Prettify<{
|
|
7891
|
+
[K in keyof T]: T[K] extends ScalarCallback<any, infer R> ? R : never;
|
|
7892
|
+
}>;
|
|
7893
|
+
/**
|
|
7894
|
+
* Type for the scalar definitions object passed to db.scalar()
|
|
7895
|
+
*/
|
|
7896
|
+
export type ScalarDefinitions<TSchema> = Record<string, ScalarCallback<TSchema, any>>;
|
|
7897
|
+
/**
|
|
7898
|
+
* Executor function type for running queries
|
|
7899
|
+
*/
|
|
7900
|
+
export type QueryExecutor = (sql: string) => Promise<{
|
|
7901
|
+
data: any;
|
|
7902
|
+
metadata: RelqMetadata;
|
|
7903
|
+
}>;
|
|
7904
|
+
/**
|
|
7905
|
+
* Column resolver getter type
|
|
7906
|
+
*/
|
|
7907
|
+
export type ColumnResolverGetter = (tableName: string) => ((column: string) => string) | undefined;
|
|
7908
|
+
declare class ScalarSelectBuilder<TSchema, TScalars extends Record<string, ScalarCallback<TSchema, any>>> {
|
|
7909
|
+
private readonly executeQuery;
|
|
7910
|
+
private readonly scalarResults;
|
|
7911
|
+
constructor(scalars: TScalars, schema: TSchema, executeQuery: QueryExecutor, getColumnResolver?: ColumnResolverGetter);
|
|
7912
|
+
/**
|
|
7913
|
+
* Generate the SQL for this scalar select
|
|
7914
|
+
*/
|
|
7915
|
+
toString(): string;
|
|
7916
|
+
/**
|
|
7917
|
+
* Execute the query and return the result
|
|
7918
|
+
* @param withMetadata - If true, returns { data, metadata } instead of just data
|
|
7919
|
+
*/
|
|
7920
|
+
get(withMetadata?: false): Promise<InferScalars<TScalars>>;
|
|
7921
|
+
get(withMetadata: true): Promise<RelqResult<InferScalars<TScalars>>>;
|
|
7922
|
+
/**
|
|
7923
|
+
* Transform result values to correct types
|
|
7924
|
+
*/
|
|
7925
|
+
private transformResult;
|
|
7926
|
+
}
|
|
7927
|
+
declare class ConnectedScalarSelectBuilder<TSchema, TScalars extends Record<string, ScalarCallback<TSchema, any>>> extends ScalarSelectBuilder<TSchema, TScalars> {
|
|
7928
|
+
constructor(scalars: TScalars, schema: TSchema, internal: {
|
|
7929
|
+
executeSelectOne: (sql: string) => Promise<{
|
|
7930
|
+
data: any;
|
|
7931
|
+
metadata: RelqMetadata;
|
|
7932
|
+
}>;
|
|
7933
|
+
hasColumnMapping: () => boolean;
|
|
7934
|
+
transformToDbColumns: (tableName: string, data: Record<string, any>) => Record<string, any>;
|
|
7935
|
+
});
|
|
7936
|
+
}
|
|
7783
7937
|
/**
|
|
7784
7938
|
* Main Relq client class with connection management
|
|
7785
7939
|
* Supports both pooled and single connection modes
|
|
@@ -8174,6 +8328,26 @@ export declare class Relq<TSchema = any> {
|
|
|
8174
8328
|
verbose?: boolean;
|
|
8175
8329
|
format?: "text" | "json" | "xml" | "yaml";
|
|
8176
8330
|
}): Promise<string | object[]>;
|
|
8331
|
+
/**
|
|
8332
|
+
* Execute multiple scalar subqueries in a single SELECT statement
|
|
8333
|
+
*
|
|
8334
|
+
* Use this for aggregating values from multiple tables without a base table.
|
|
8335
|
+
* Each callback receives a table accessor and must return a scalar result
|
|
8336
|
+
* (count, sum, avg, min, max, pick, or exists).
|
|
8337
|
+
*
|
|
8338
|
+
* @example
|
|
8339
|
+
* ```typescript
|
|
8340
|
+
* const stats = await db.scalar({
|
|
8341
|
+
* licenseCount: q => q.licenses.where(w => w.equal('productId', id)).count(),
|
|
8342
|
+
* totalSpent: q => q.orders.where(w => w.equal('customerId', id)).sum('amount'),
|
|
8343
|
+
* lastOrder: q => q.orders.where(w => w.equal('customerId', id)).max('createdAt'),
|
|
8344
|
+
* hasSubscription: q => q.subscriptions.where(w => w.equal('userId', id)).exists(),
|
|
8345
|
+
* }).get();
|
|
8346
|
+
*
|
|
8347
|
+
* // Type: { licenseCount: number, totalSpent: number, lastOrder: Date, hasSubscription: boolean }
|
|
8348
|
+
* ```
|
|
8349
|
+
*/
|
|
8350
|
+
scalar<T extends Record<string, ScalarCallback<TSchema, any>>>(scalars: T): ConnectedScalarSelectBuilder<TSchema, T>;
|
|
8177
8351
|
}
|
|
8178
8352
|
/**
|
|
8179
8353
|
* Type inference for aggregate functions, COUNT queries, and GROUP BY operations
|