@carbonorm/carbonnode 3.8.4 → 3.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -1
- package/dist/api/C6Constants.d.ts +6 -0
- package/dist/api/orm/builders/AggregateBuilder.d.ts +2 -1
- package/dist/api/orm/builders/ConditionBuilder.d.ts +3 -0
- package/dist/api/orm/builders/JoinBuilder.d.ts +8 -0
- package/dist/api/orm/builders/PaginationBuilder.d.ts +1 -1
- package/dist/api/orm/queries/DeleteQueryBuilder.d.ts +2 -0
- package/dist/api/orm/queries/SelectQueryBuilder.d.ts +1 -0
- package/dist/api/orm/queries/UpdateQueryBuilder.d.ts +2 -0
- package/dist/api/orm/queryHelpers.d.ts +5 -0
- package/dist/index.cjs.js +409 -114
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +407 -115
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/fixtures/c6.fixture.ts +39 -0
- package/src/__tests__/fixtures/pu.fixture.ts +72 -0
- package/src/__tests__/sakila-db/C6.js +1 -1
- package/src/__tests__/sakila-db/C6.ts +1 -1
- package/src/__tests__/sqlBuilders.complex.test.ts +281 -1
- package/src/api/C6Constants.ts +4 -1
- package/src/api/orm/builders/AggregateBuilder.ts +70 -2
- package/src/api/orm/builders/ConditionBuilder.ts +66 -4
- package/src/api/orm/builders/JoinBuilder.ts +117 -6
- package/src/api/orm/builders/PaginationBuilder.ts +19 -2
- package/src/api/orm/queries/DeleteQueryBuilder.ts +5 -0
- package/src/api/orm/queries/SelectQueryBuilder.ts +6 -2
- package/src/api/orm/queries/UpdateQueryBuilder.ts +6 -1
- package/src/api/orm/queryHelpers.ts +59 -0
|
@@ -1,20 +1,88 @@
|
|
|
1
1
|
import {OrmGenerics} from "../../types/ormGenerics";
|
|
2
2
|
import {ConditionBuilder} from "./ConditionBuilder";
|
|
3
|
+
import {C6C} from "../../C6Constants";
|
|
4
|
+
import {resolveDerivedTable, isDerivedTableKey} from "../queryHelpers";
|
|
3
5
|
|
|
4
6
|
export abstract class JoinBuilder<G extends OrmGenerics> extends ConditionBuilder<G>{
|
|
5
7
|
|
|
8
|
+
protected createSelectBuilder(
|
|
9
|
+
_request: any
|
|
10
|
+
): { build(table: string, isSubSelect: boolean): { sql: string; params: any[] | Record<string, any> } } {
|
|
11
|
+
throw new Error('Subclasses must implement createSelectBuilder to support derived table serialization.');
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
buildJoinClauses(joinArgs: any, params: any[] | Record<string, any>): string {
|
|
7
15
|
let sql = '';
|
|
8
16
|
|
|
9
17
|
for (const joinType in joinArgs) {
|
|
10
18
|
const joinKind = joinType.replace('_', ' ').toUpperCase();
|
|
19
|
+
const entries: Array<[any, any]> = [];
|
|
20
|
+
const joinSection = joinArgs[joinType];
|
|
21
|
+
|
|
22
|
+
if (joinSection instanceof Map) {
|
|
23
|
+
joinSection.forEach((value, key) => {
|
|
24
|
+
entries.push([key, value]);
|
|
25
|
+
});
|
|
26
|
+
} else {
|
|
27
|
+
for (const raw in joinSection) {
|
|
28
|
+
entries.push([raw, joinSection[raw]]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const [rawKey, conditions] of entries) {
|
|
33
|
+
const raw = typeof rawKey === 'string' ? rawKey : String(rawKey);
|
|
34
|
+
const [table, aliasCandidate] = raw.trim().split(/\s+/, 2);
|
|
35
|
+
if (!table) continue;
|
|
36
|
+
|
|
37
|
+
if (isDerivedTableKey(table)) {
|
|
38
|
+
const derived = resolveDerivedTable(table);
|
|
39
|
+
if (!derived) {
|
|
40
|
+
throw new Error(`Derived table '${table}' was not registered. Wrap the object with derivedTable(...) before using it in JOIN.`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const configuredAliasRaw = derived[C6C.AS];
|
|
44
|
+
const configuredAlias = typeof configuredAliasRaw === 'string' ? configuredAliasRaw.trim() : '';
|
|
45
|
+
const alias = (aliasCandidate ?? configuredAlias).trim();
|
|
46
|
+
|
|
47
|
+
if (!alias) {
|
|
48
|
+
throw new Error('Derived tables require an alias via C6C.AS.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.registerAlias(alias, table);
|
|
52
|
+
|
|
53
|
+
const subRequest = derived[C6C.SUBSELECT];
|
|
54
|
+
if (!subRequest || typeof subRequest !== 'object') {
|
|
55
|
+
throw new Error('Derived tables must include a C6C.SUBSELECT payload.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const fromTable = subRequest[C6C.FROM];
|
|
59
|
+
if (typeof fromTable !== 'string' || fromTable.trim() === '') {
|
|
60
|
+
throw new Error('Derived table subselects require a base table defined with C6C.FROM.');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const subBuilder = this.createSelectBuilder(subRequest as any);
|
|
64
|
+
const { sql: subSql, params: subParams } = subBuilder.build(fromTable, true);
|
|
65
|
+
const normalizedSql = this.integrateSubSelectParams(subSql, subParams, params);
|
|
11
66
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
67
|
+
const formatted = normalizedSql.trim().split('\n').map(line => ` ${line}`).join('\n');
|
|
68
|
+
const joinSql = `(\n${formatted}\n) AS \`${alias}\``;
|
|
69
|
+
const onClause = this.buildBooleanJoinedConditions(conditions, true, params);
|
|
70
|
+
sql += ` ${joinKind} JOIN ${joinSql}`;
|
|
71
|
+
if (onClause) {
|
|
72
|
+
sql += ` ON ${onClause}`;
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
const alias = aliasCandidate;
|
|
76
|
+
if (alias) {
|
|
77
|
+
this.registerAlias(alias, table);
|
|
78
|
+
}
|
|
79
|
+
const joinSql = alias ? `\`${table}\` AS \`${alias}\`` : `\`${table}\``;
|
|
80
|
+
const onClause = this.buildBooleanJoinedConditions(conditions, true, params);
|
|
81
|
+
sql += ` ${joinKind} JOIN ${joinSql}`;
|
|
82
|
+
if (onClause) {
|
|
83
|
+
sql += ` ON ${onClause}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
18
86
|
}
|
|
19
87
|
}
|
|
20
88
|
|
|
@@ -22,4 +90,47 @@ export abstract class JoinBuilder<G extends OrmGenerics> extends ConditionBuilde
|
|
|
22
90
|
|
|
23
91
|
return sql;
|
|
24
92
|
}
|
|
93
|
+
|
|
94
|
+
protected integrateSubSelectParams(
|
|
95
|
+
subSql: string,
|
|
96
|
+
subParams: any[] | Record<string, any>,
|
|
97
|
+
target: any[] | Record<string, any>
|
|
98
|
+
): string {
|
|
99
|
+
if (!subParams) return subSql;
|
|
100
|
+
|
|
101
|
+
if (this.useNamedParams) {
|
|
102
|
+
let normalized = subSql;
|
|
103
|
+
const extras = subParams as Record<string, any>;
|
|
104
|
+
for (const key of Object.keys(extras)) {
|
|
105
|
+
const placeholder = this.addParam(target, '', extras[key]);
|
|
106
|
+
const original = `:${key}`;
|
|
107
|
+
if (original !== placeholder) {
|
|
108
|
+
normalized = normalized.split(original).join(placeholder);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return normalized;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
(target as any[]).push(...(subParams as any[]));
|
|
115
|
+
return subSql;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
protected buildScalarSubSelect(
|
|
119
|
+
subRequest: any,
|
|
120
|
+
params: any[] | Record<string, any>
|
|
121
|
+
): string {
|
|
122
|
+
if (!subRequest || typeof subRequest !== 'object') {
|
|
123
|
+
throw new Error('Scalar subselect requires a C6C.SUBSELECT object payload.');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const fromTable = subRequest[C6C.FROM];
|
|
127
|
+
if (typeof fromTable !== 'string' || fromTable.trim() === '') {
|
|
128
|
+
throw new Error('Scalar subselects require a base table specified with C6C.FROM.');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const subBuilder = this.createSelectBuilder(subRequest as any);
|
|
132
|
+
const { sql: subSql, params: subParams } = subBuilder.build(fromTable, true);
|
|
133
|
+
const normalized = this.integrateSubSelectParams(subSql, subParams, params).trim();
|
|
134
|
+
return `(${normalized})`;
|
|
135
|
+
}
|
|
25
136
|
}
|
|
@@ -17,7 +17,7 @@ export abstract class PaginationBuilder<G extends OrmGenerics> extends JoinBuild
|
|
|
17
17
|
* }
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
|
-
buildPaginationClause(pagination: any): string {
|
|
20
|
+
buildPaginationClause(pagination: any, params?: any[] | Record<string, any>): string {
|
|
21
21
|
let sql = "";
|
|
22
22
|
|
|
23
23
|
/* -------- ORDER BY -------- */
|
|
@@ -25,10 +25,27 @@ export abstract class PaginationBuilder<G extends OrmGenerics> extends JoinBuild
|
|
|
25
25
|
const orderParts: string[] = [];
|
|
26
26
|
|
|
27
27
|
for (const [key, val] of Object.entries(pagination[C6Constants.ORDER])) {
|
|
28
|
+
if (typeof key === 'string' && key.includes('.')) {
|
|
29
|
+
this.assertValidIdentifier(key, 'ORDER BY');
|
|
30
|
+
}
|
|
28
31
|
// FUNCTION CALL: val is an array of args
|
|
29
32
|
if (Array.isArray(val)) {
|
|
33
|
+
const identifierPathRegex = /^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/;
|
|
34
|
+
const isNumericString = (s: string) => /^-?\d+(?:\.\d+)?$/.test(s.trim());
|
|
30
35
|
const args = val
|
|
31
|
-
.map((arg) =>
|
|
36
|
+
.map((arg) => {
|
|
37
|
+
if (Array.isArray(arg)) return this.buildAggregateField(arg, params);
|
|
38
|
+
if (typeof arg === 'string') {
|
|
39
|
+
if (identifierPathRegex.test(arg)) {
|
|
40
|
+
this.assertValidIdentifier(arg, 'ORDER BY argument');
|
|
41
|
+
return arg;
|
|
42
|
+
}
|
|
43
|
+
// numeric-looking strings should be treated as literals
|
|
44
|
+
if (isNumericString(arg)) return arg;
|
|
45
|
+
return arg;
|
|
46
|
+
}
|
|
47
|
+
return String(arg);
|
|
48
|
+
})
|
|
32
49
|
.join(", ");
|
|
33
50
|
orderParts.push(`${key}(${args})`);
|
|
34
51
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { OrmGenerics } from "../../types/ormGenerics";
|
|
2
2
|
import { SqlBuilderResult } from "../utils/sqlUtils";
|
|
3
3
|
import { JoinBuilder } from "../builders/JoinBuilder";
|
|
4
|
+
import { SelectQueryBuilder } from "./SelectQueryBuilder";
|
|
4
5
|
|
|
5
6
|
export class DeleteQueryBuilder<G extends OrmGenerics> extends JoinBuilder<G> {
|
|
7
|
+
protected createSelectBuilder(request: any) {
|
|
8
|
+
return new SelectQueryBuilder(this.config as any, request, this.useNamedParams);
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
build(
|
|
7
12
|
table: string
|
|
8
13
|
): SqlBuilderResult {
|
|
@@ -4,6 +4,10 @@ import {SqlBuilderResult} from "../utils/sqlUtils";
|
|
|
4
4
|
|
|
5
5
|
export class SelectQueryBuilder<G extends OrmGenerics> extends PaginationBuilder<G>{
|
|
6
6
|
|
|
7
|
+
protected createSelectBuilder(request: any) {
|
|
8
|
+
return new SelectQueryBuilder(this.config as any, request, this.useNamedParams);
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
build(
|
|
8
12
|
table: string,
|
|
9
13
|
isSubSelect: boolean = false
|
|
@@ -17,7 +21,7 @@ export class SelectQueryBuilder<G extends OrmGenerics> extends PaginationBuilder
|
|
|
17
21
|
const params = this.useNamedParams ? {} : [];
|
|
18
22
|
const selectList = args.SELECT ?? ['*'];
|
|
19
23
|
const selectFields = selectList
|
|
20
|
-
.map((f: any) => this.buildAggregateField(f))
|
|
24
|
+
.map((f: any) => this.buildAggregateField(f, params))
|
|
21
25
|
.join(', ');
|
|
22
26
|
|
|
23
27
|
let sql = `SELECT ${selectFields} FROM \`${table}\``;
|
|
@@ -42,7 +46,7 @@ export class SelectQueryBuilder<G extends OrmGenerics> extends PaginationBuilder
|
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
if (args.PAGINATION) {
|
|
45
|
-
sql += this.buildPaginationClause(args.PAGINATION);
|
|
49
|
+
sql += this.buildPaginationClause(args.PAGINATION, params);
|
|
46
50
|
} else if (!isSubSelect) {
|
|
47
51
|
sql += ` LIMIT 100`;
|
|
48
52
|
}
|
|
@@ -2,8 +2,13 @@ import {C6C} from "../../C6Constants";
|
|
|
2
2
|
import {OrmGenerics} from "../../types/ormGenerics";
|
|
3
3
|
import { PaginationBuilder } from '../builders/PaginationBuilder';
|
|
4
4
|
import {SqlBuilderResult} from "../utils/sqlUtils";
|
|
5
|
+
import {SelectQueryBuilder} from "./SelectQueryBuilder";
|
|
5
6
|
|
|
6
7
|
export class UpdateQueryBuilder<G extends OrmGenerics> extends PaginationBuilder<G>{
|
|
8
|
+
protected createSelectBuilder(request: any) {
|
|
9
|
+
return new SelectQueryBuilder(this.config as any, request, this.useNamedParams);
|
|
10
|
+
}
|
|
11
|
+
|
|
7
12
|
private trimTablePrefix(table: string, column: string): string {
|
|
8
13
|
if (!column.includes('.')) return column;
|
|
9
14
|
const [prefix, col] = column.split('.', 2);
|
|
@@ -44,7 +49,7 @@ export class UpdateQueryBuilder<G extends OrmGenerics> extends PaginationBuilder
|
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
if (args.PAGINATION) {
|
|
47
|
-
sql += this.buildPaginationClause(args.PAGINATION);
|
|
52
|
+
sql += this.buildPaginationClause(args.PAGINATION, params);
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
return { sql, params };
|
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
// Alias a table name with a given alias
|
|
2
2
|
import {C6C} from "../C6Constants";
|
|
3
3
|
|
|
4
|
+
type DerivedTableSpec = Record<string, any> & {
|
|
5
|
+
[C6C.SUBSELECT]?: Record<string, any>;
|
|
6
|
+
[C6C.AS]?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const DERIVED_TABLE_PREFIX = '__c6DerivedTable__';
|
|
10
|
+
const DERIVED_ID_SYMBOL = Symbol('c6DerivedTableId');
|
|
11
|
+
|
|
12
|
+
const derivedTableLookup = new Map<string, DerivedTableSpec>();
|
|
13
|
+
const derivedTableReverseLookup = new WeakMap<DerivedTableSpec, string>();
|
|
14
|
+
let derivedTableCounter = 0;
|
|
15
|
+
|
|
16
|
+
export const isDerivedTableKey = (key: string): boolean =>
|
|
17
|
+
typeof key === 'string' && key.startsWith(DERIVED_TABLE_PREFIX);
|
|
18
|
+
|
|
19
|
+
export const resolveDerivedTable = (key: string): DerivedTableSpec | undefined =>
|
|
20
|
+
derivedTableLookup.get(key);
|
|
21
|
+
|
|
22
|
+
export const derivedTable = <T extends DerivedTableSpec>(spec: T): T => {
|
|
23
|
+
if (!spec || typeof spec !== 'object') {
|
|
24
|
+
throw new Error('Derived table definition must be an object.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const aliasRaw = spec[C6C.AS];
|
|
28
|
+
if (typeof aliasRaw !== 'string' || aliasRaw.trim() === '') {
|
|
29
|
+
throw new Error('Derived tables require a non-empty alias via C6C.AS.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!spec[C6C.SUBSELECT] || typeof spec[C6C.SUBSELECT] !== 'object') {
|
|
33
|
+
throw new Error('Derived tables require a nested SELECT payload under C6C.SUBSELECT.');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let id = derivedTableReverseLookup.get(spec);
|
|
37
|
+
|
|
38
|
+
if (!id) {
|
|
39
|
+
id = `${DERIVED_TABLE_PREFIX}${++derivedTableCounter}`;
|
|
40
|
+
derivedTableReverseLookup.set(spec, id);
|
|
41
|
+
derivedTableLookup.set(id, spec);
|
|
42
|
+
Object.defineProperty(spec, DERIVED_ID_SYMBOL, {
|
|
43
|
+
value: id,
|
|
44
|
+
configurable: false,
|
|
45
|
+
enumerable: false,
|
|
46
|
+
writable: false
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const alias = aliasRaw.trim();
|
|
51
|
+
derivedTableLookup.set(id!, spec);
|
|
52
|
+
|
|
53
|
+
Object.defineProperty(spec, 'toString', {
|
|
54
|
+
value: () => `${id} ${alias}`,
|
|
55
|
+
configurable: true,
|
|
56
|
+
enumerable: false,
|
|
57
|
+
writable: true
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return spec;
|
|
61
|
+
};
|
|
62
|
+
|
|
4
63
|
export const A = (tableName: string, alias: string): string =>
|
|
5
64
|
`${tableName} ${alias}`;
|
|
6
65
|
|