@carbonorm/carbonnode 3.7.6 → 3.7.7

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.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/dist/api/orm/builders/AggregateBuilder.d.ts +1 -0
  3. package/dist/api/types/ormInterfaces.d.ts +2 -2
  4. package/dist/api/utils/cacheManager.d.ts +1 -1
  5. package/dist/api/utils/normalizeSingularRequest.d.ts +10 -0
  6. package/dist/index.cjs.js +175 -32
  7. package/dist/index.cjs.js.map +1 -1
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.esm.js +176 -34
  10. package/dist/index.esm.js.map +1 -1
  11. package/package.json +11 -6
  12. package/scripts/assets/handlebars/C6.test.ts.handlebars +55 -80
  13. package/scripts/assets/handlebars/C6.ts.handlebars +28 -2
  14. package/scripts/generateRestBindings.cjs +17 -6
  15. package/scripts/generateRestBindings.ts +22 -8
  16. package/src/__tests__/fixtures/c6.fixture.ts +74 -0
  17. package/src/__tests__/normalizeSingularRequest.test.ts +95 -0
  18. package/src/__tests__/sakila-db/C6.js +1487 -0
  19. package/src/__tests__/sakila-db/C6.test.ts +63 -0
  20. package/src/__tests__/sakila-db/C6.ts +2206 -0
  21. package/src/__tests__/sakila-db/sakila-data.sql +46444 -0
  22. package/src/__tests__/sakila-db/sakila-schema.sql +686 -0
  23. package/src/__tests__/sakila-db/sakila.mwb +0 -0
  24. package/src/__tests__/sakila.generated.test.ts +46 -0
  25. package/src/__tests__/sqlBuilders.complex.test.ts +134 -0
  26. package/src/__tests__/sqlBuilders.test.ts +121 -0
  27. package/src/api/convertForRequestBody.ts +1 -1
  28. package/src/api/executors/HttpExecutor.ts +14 -3
  29. package/src/api/executors/SqlExecutor.ts +14 -1
  30. package/src/api/orm/builders/AggregateBuilder.ts +3 -0
  31. package/src/api/orm/builders/ConditionBuilder.ts +34 -11
  32. package/src/api/orm/builders/PaginationBuilder.ts +10 -4
  33. package/src/api/orm/queries/SelectQueryBuilder.ts +3 -0
  34. package/src/api/orm/queries/UpdateQueryBuilder.ts +2 -1
  35. package/src/api/types/ormInterfaces.ts +3 -4
  36. package/src/api/utils/cacheManager.ts +1 -1
  37. package/src/api/utils/normalizeSingularRequest.ts +138 -0
  38. package/src/index.ts +1 -0
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect, beforeAll, vi } from 'vitest';
2
+
3
+ // Import the generated C6.js from sakila-db folder (ESM)
4
+ // This file is generated from the Sakila schema and wired with restOrm
5
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
6
+ // @ts-ignore - C6.js is JS, vitest/ts handles ESM imports fine
7
+ import { C6, GLOBAL_REST_PARAMETERS } from './sakila-db/C6.js';
8
+
9
+ function toPascalCase(name: string) {
10
+ return name.replace(/(^|_)([a-z])/g, (_m, _u, c) => c.toUpperCase());
11
+ }
12
+
13
+ describe('sakila-db generated C6 bindings', () => {
14
+ beforeAll(() => {
15
+ // Provide a mocked MySQL pool so SqlExecutor path is used without a real DB
16
+ const mockConn = {
17
+ query: vi.fn().mockImplementation(async (_sql: string, _values?: any[]) => {
18
+ // Return a result set shaped like mysql2/promise: [rows, fields]
19
+ return [[{ ok: true }], []];
20
+ }),
21
+ release: vi.fn()
22
+ };
23
+
24
+ const mockPool = {
25
+ getConnection: vi.fn().mockResolvedValue(mockConn)
26
+ } as any;
27
+
28
+ // Inject mocked pool into global rest parameters used by all table bindings
29
+ GLOBAL_REST_PARAMETERS.mysqlPool = mockPool;
30
+ });
31
+
32
+ it('Get(...LIMIT...) returns array rest for every generated table', async () => {
33
+ // Iterate over each table short name present in generated C6
34
+ for (const [shortName] of Object.entries(C6.TABLES as Record<string, any>)) {
35
+ const bindingName = toPascalCase(shortName);
36
+ const restBinding = (C6.ORM as Record<string, any>)[bindingName];
37
+ if (!restBinding || typeof restBinding.Get !== 'function') continue;
38
+
39
+ const response = await restBinding.Get({ SELECT: ['*'], [C6.PAGINATION]: { [C6.LIMIT]: 1 } } as any);
40
+
41
+ // HttpExecutor returns AxiosResponse, SqlExecutor returns plain object
42
+ const data = (response as any)?.data ?? response;
43
+ expect(Array.isArray((data as any)?.rest)).toBe(true);
44
+ }
45
+ });
46
+ });
@@ -0,0 +1,134 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { C6C } from '../api/C6Constants';
3
+ import { SelectQueryBuilder } from '../api/orm/queries/SelectQueryBuilder';
4
+ import { buildTestConfig } from './fixtures/c6.fixture';
5
+
6
+ /**
7
+ * Complex SELECT coverage focused on WHERE operators, JOIN chains, ORDER, and pagination.
8
+ */
9
+ describe('SQL Builders - Complex SELECTs', () => {
10
+ it('supports nested AND/OR groups, IN/NOT IN, BETWEEN, IS NULL', () => {
11
+ const config = buildTestConfig();
12
+
13
+ const qb = new SelectQueryBuilder(config as any, {
14
+ SELECT: ['actor.actor_id', 'actor.first_name'],
15
+ WHERE: {
16
+ // AND root with one direct condition
17
+ 'actor.first_name': [C6C.LIKE, 'A%'],
18
+ // OR group #1
19
+ 0: {
20
+ 'actor.actor_id': [C6C.IN, [1, 2, 3]]
21
+ },
22
+ // OR group #2
23
+ 1: {
24
+ 'actor.actor_id': [C6C.BETWEEN, [5, 10]]
25
+ },
26
+ // OR group #3
27
+ 2: {
28
+ 'actor.last_name': [C6C.IS, null]
29
+ },
30
+ // AND with NOT IN
31
+ 'actor.last_name': [C6C.NOT_IN, ['SMITH', 'DOE']]
32
+ }
33
+ } as any, false);
34
+
35
+ const { sql, params } = qb.build('actor');
36
+
37
+ // SQL fragments
38
+ expect(sql).toContain('SELECT actor.actor_id, actor.first_name FROM `actor`');
39
+ expect(sql).toContain('WHERE');
40
+ expect(sql).toMatch(/\(actor\.first_name\) LIKE \?/);
41
+ expect(sql).toMatch(/\( actor\.actor_id IN \((\?,\s*){2}\?\) \)/); // 3 placeholders
42
+ expect(sql).toMatch(/\(actor\.actor_id\) BETWEEN \? AND \?/);
43
+ expect(sql).toMatch(/\(actor\.last_name\) IS \?/);
44
+ expect(sql).toMatch(/\( actor\.last_name NOT IN \((\?,\s*)\?\) \)/);
45
+ // default LIMIT
46
+ expect(sql.trim().endsWith('LIMIT 100')).toBe(true);
47
+
48
+ // Params order: non-numeric entries first, then grouped (implementation detail)
49
+ // We asserted shape above; ensure counts match
50
+ expect(params).toHaveLength(1 + 3 + 2 + 1 + 2); // LIKE + IN(3) + BETWEEN(2) + IS(1 param) + NOT IN(2)
51
+ expect(params[0]).toBe('A%');
52
+ });
53
+
54
+ it('builds chained mixed JOINs with aliases', () => {
55
+ const config = buildTestConfig();
56
+
57
+ const qb = new SelectQueryBuilder(config as any, {
58
+ SELECT: ['actor.actor_id'],
59
+ JOIN: {
60
+ [C6C.INNER]: {
61
+ 'film_actor fa': { 'fa.actor_id': [C6C.EQUAL, 'actor.actor_id'] }
62
+ },
63
+ [C6C.LEFT]: {
64
+ 'film_actor fb': { 'fb.actor_id': [C6C.EQUAL, 'actor.actor_id'] }
65
+ }
66
+ },
67
+ WHERE: { 'actor.actor_id': [C6C.GREATER_THAN, 0] }
68
+ } as any, false);
69
+
70
+ const { sql, params } = qb.build('actor');
71
+
72
+ expect(sql).toContain('FROM `actor`');
73
+ expect(sql).toContain('INNER JOIN `film_actor` AS `fa` ON');
74
+ expect(sql).toContain('LEFT JOIN `film_actor` AS `fb` ON');
75
+ expect(sql).toMatch(/\(actor\.actor_id\) > \?/);
76
+ expect(params).toEqual([0]);
77
+ });
78
+
79
+ it('orders by multiple fields and honors PAGE/LIMIT offset', () => {
80
+ const config = buildTestConfig();
81
+
82
+ const qb = new SelectQueryBuilder(config as any, {
83
+ SELECT: ['actor.actor_id', 'actor.first_name'],
84
+ WHERE: { 'actor.actor_id': [C6C.GREATER_THAN, 10] },
85
+ PAGINATION: {
86
+ [C6C.ORDER]: {
87
+ 'actor.last_name': 'ASC',
88
+ 'actor.first_name': 'DESC'
89
+ },
90
+ [C6C.LIMIT]: 10,
91
+ [C6C.PAGE]: 3
92
+ }
93
+ } as any, false);
94
+
95
+ const { sql, params } = qb.build('actor');
96
+
97
+ expect(sql).toContain('ORDER BY actor.last_name ASC, actor.first_name DESC');
98
+ expect(sql.trim().endsWith('LIMIT 20, 10')).toBe(true); // (page-1)*limit, limit
99
+ expect(params).toEqual([10]);
100
+ });
101
+
102
+ it('supports DISTINCT and HAVING on aggregated alias', () => {
103
+ const config = buildTestConfig();
104
+
105
+ const qb = new SelectQueryBuilder(config as any, {
106
+ SELECT: [[C6C.DISTINCT, 'actor.first_name'], [C6C.COUNT, 'actor.actor_id', C6C.AS, 'cnt']],
107
+ GROUP_BY: 'actor.first_name',
108
+ HAVING: { 'cnt': [C6C.GREATER_THAN, 1] }
109
+ } as any, false);
110
+
111
+ const { sql, params } = qb.build('actor');
112
+
113
+ expect(sql).toContain('SELECT DISTINCT actor.first_name, COUNT(actor.actor_id) AS cnt FROM `actor`');
114
+ expect(sql).toContain('GROUP BY actor.first_name');
115
+ expect(sql).toContain('HAVING');
116
+ expect(params).toEqual([1]);
117
+ });
118
+
119
+ it('supports MATCH_AGAINST fulltext condition variants', () => {
120
+ const config = buildTestConfig();
121
+
122
+ const qb = new SelectQueryBuilder(config as any, {
123
+ SELECT: ['actor.actor_id'],
124
+ WHERE: {
125
+ 'actor.first_name': [C6C.MATCH_AGAINST, ['alpha beta', 'BOOLEAN']]
126
+ }
127
+ } as any, false);
128
+
129
+ const { sql, params } = qb.build('actor');
130
+
131
+ expect(sql).toMatch(/MATCH\(actor\.first_name\) AGAINST\(\? IN BOOLEAN MODE\)/);
132
+ expect(params).toEqual(['alpha beta']);
133
+ });
134
+ });
@@ -0,0 +1,121 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { C6C } from '../api/C6Constants';
3
+ import { SelectQueryBuilder } from '../api/orm/queries/SelectQueryBuilder';
4
+ import { PostQueryBuilder } from '../api/orm/queries/PostQueryBuilder';
5
+ import { UpdateQueryBuilder } from '../api/orm/queries/UpdateQueryBuilder';
6
+ import { DeleteQueryBuilder } from '../api/orm/queries/DeleteQueryBuilder';
7
+ import { buildTestConfig } from './fixtures/c6.fixture';
8
+
9
+ describe('SQL Builders', () => {
10
+ it('builds SELECT with JOIN, WHERE, GROUP BY, HAVING and default LIMIT', () => {
11
+ const config = buildTestConfig();
12
+ // named params disabled -> positional params array
13
+ const qb = new SelectQueryBuilder(config as any, {
14
+ SELECT: ['actor.first_name', [C6C.COUNT, 'actor.actor_id', C6C.AS, 'cnt']],
15
+ JOIN: {
16
+ [C6C.INNER]: {
17
+ 'film_actor fa': {
18
+ 'fa.actor_id': [C6C.EQUAL, 'actor.actor_id']
19
+ }
20
+ }
21
+ },
22
+ WHERE: {
23
+ 'actor.first_name': [C6C.LIKE, '%A%'],
24
+ 0: {
25
+ 'actor.actor_id': [C6C.GREATER_THAN, 10],
26
+ }
27
+ },
28
+ GROUP_BY: 'actor.first_name',
29
+ HAVING: {
30
+ 'cnt': [C6C.GREATER_THAN, 1]
31
+ },
32
+ } as any, false);
33
+
34
+ const { sql, params } = qb.build('actor');
35
+
36
+ expect(sql).toContain('SELECT actor.first_name, COUNT(actor.actor_id) AS cnt FROM `actor`');
37
+ expect(sql).toContain('INNER JOIN `film_actor` AS `fa` ON');
38
+ expect(sql).toContain('(actor.first_name) LIKE ?');
39
+ expect(sql).toContain('(actor.actor_id) > ?');
40
+ expect(sql).toContain('GROUP BY actor.first_name');
41
+ expect(sql).toContain('HAVING');
42
+ expect(sql.trim().endsWith('LIMIT 100')).toBe(true);
43
+ expect(params).toEqual(['%A%', 10, 1]);
44
+ });
45
+
46
+ it('builds INSERT with ON DUPLICATE KEY UPDATE', () => {
47
+ const config = buildTestConfig();
48
+ const qb = new PostQueryBuilder(config as any, {
49
+ [C6C.REPLACE]: {
50
+ 'actor.first_name': 'BOB',
51
+ 'actor.last_name': 'SMITH',
52
+ },
53
+ [C6C.UPDATE]: ['first_name', 'last_name'],
54
+ } as any, false);
55
+
56
+ const { sql, params } = qb.build('actor');
57
+
58
+ expect(sql).toContain('REPLACE INTO `actor`');
59
+ expect(sql).toContain('`first_name`, `last_name`');
60
+ expect(sql).toContain('ON DUPLICATE KEY UPDATE `first_name` = VALUES(`first_name`), `last_name` = VALUES(`last_name`)');
61
+ expect(params).toEqual(['BOB', 'SMITH']);
62
+ });
63
+
64
+ it('builds UPDATE with WHERE and pagination', () => {
65
+ const config = buildTestConfig();
66
+ const qb = new UpdateQueryBuilder(config as any, {
67
+ [C6C.UPDATE]: {
68
+ 'first_name': 'ALICE',
69
+ },
70
+ WHERE: {
71
+ 'actor.actor_id': [C6C.EQUAL, 5],
72
+ },
73
+ PAGINATION: { LIMIT: 1 }
74
+ } as any, false);
75
+
76
+ const { sql, params } = qb.build('actor');
77
+
78
+ expect(sql.startsWith('UPDATE `actor` SET')).toBe(true);
79
+ expect(sql).toContain('`first_name` = ?');
80
+ expect(sql).toContain('WHERE (actor.actor_id) = ?');
81
+ expect(sql).toContain('LIMIT 1');
82
+ expect(params).toEqual(['ALICE', 5]);
83
+ });
84
+
85
+ it('builds DELETE with JOIN and WHERE', () => {
86
+ const config = buildTestConfig();
87
+ const qb = new DeleteQueryBuilder(config as any, {
88
+ JOIN: {
89
+ [C6C.INNER]: {
90
+ 'film_actor fa': {
91
+ 'fa.actor_id': [C6C.EQUAL, 'actor.actor_id']
92
+ }
93
+ }
94
+ },
95
+ WHERE: { 'actor.actor_id': [C6C.GREATER_THAN, 100] },
96
+ } as any, false);
97
+
98
+ const { sql, params } = qb.build('actor');
99
+
100
+ expect(sql).toContain('DELETE `actor` FROM `actor`');
101
+ expect(sql).toContain('INNER JOIN `film_actor` AS `fa` ON');
102
+ expect(sql).toContain('(actor.actor_id) > ?');
103
+ expect(params).toEqual([100]);
104
+ });
105
+
106
+ it('converts hex to Buffer for BINARY columns in WHERE params', () => {
107
+ const config = buildTestConfig();
108
+ const qb = new SelectQueryBuilder(config as any, {
109
+ WHERE: {
110
+ 'actor.binarycol': [C6C.EQUAL, '0123456789abcdef0123456789abcdef']
111
+ }
112
+ } as any, false);
113
+
114
+ const { params } = qb.build('actor');
115
+ expect(Array.isArray(params)).toBe(true);
116
+ const buf = (params as any[])[0];
117
+ expect(Buffer.isBuffer(buf)).toBe(true);
118
+ expect((buf as Buffer).length).toBe(16);
119
+ });
120
+ });
121
+
@@ -1,4 +1,4 @@
1
- import { C6Constants } from "api/C6Constants";
1
+ import { C6Constants } from "./C6Constants";
2
2
  import {iC6Object, C6RestfulModel, iRestMethods, RequestQueryBody} from "./types/ormInterfaces";
3
3
 
4
4
  export default function <
@@ -15,6 +15,7 @@ import {
15
15
  PUT, RequestQueryBody
16
16
  } from "../types/ormInterfaces";
17
17
  import {removeInvalidKeys, removePrefixIfExists, TestRestfulResponse} from "../utils/apiHelpers";
18
+ import { normalizeSingularRequest } from "../utils/normalizeSingularRequest";
18
19
  import {apiRequestCache, checkCache, userCustomClearCache} from "../utils/cacheManager";
19
20
  import {sortAndSerializeQueryObject} from "../utils/sortAndSerializeQueryObject";
20
21
  import {Executor} from "./Executor";
@@ -336,6 +337,7 @@ export class HttpExecutor<
336
337
  }
337
338
 
338
339
  let addBackPK: (() => void) | undefined;
340
+ let removedPrimaryKV: { key: string; value: any } | undefined;
339
341
 
340
342
  let apiResponse: G['RestTableInterface'][G['PrimaryKey']] | string | boolean | number | undefined;
341
343
 
@@ -411,6 +413,7 @@ export class HttpExecutor<
411
413
  restRequestUri += query[primaryKey] + '/'
412
414
 
413
415
  const removedPkValue = query[primaryKey];
416
+ removedPrimaryKV = { key: primaryKey, value: removedPkValue };
414
417
 
415
418
  addBackPK = () => {
416
419
  query ??= {} as RequestQueryBody<
@@ -472,11 +475,19 @@ export class HttpExecutor<
472
475
  withCredentials: withCredentials,
473
476
  };
474
477
 
478
+ // Normalize singular request (GET/PUT/DELETE) into complex ORM shape
479
+ const normalizedQuery = normalizeSingularRequest(
480
+ requestMethod as any,
481
+ query as any,
482
+ restModel as any,
483
+ removedPrimaryKV
484
+ ) as typeof query;
485
+
475
486
  switch (requestMethod) {
476
487
  case GET:
477
488
  return [{
478
489
  ...baseConfig,
479
- params: query
490
+ params: normalizedQuery
480
491
  }];
481
492
 
482
493
  case POST:
@@ -489,12 +500,12 @@ export class HttpExecutor<
489
500
  return [convert(query), baseConfig];
490
501
 
491
502
  case PUT:
492
- return [convert(query), baseConfig];
503
+ return [convert(normalizedQuery), baseConfig];
493
504
 
494
505
  case DELETE:
495
506
  return [{
496
507
  ...baseConfig,
497
- data: convert(query)
508
+ data: convert(normalizedQuery)
498
509
  }];
499
510
 
500
511
  default:
@@ -10,6 +10,7 @@ import namedPlaceholders from 'named-placeholders';
10
10
  import {PoolConnection} from 'mysql2/promise';
11
11
  import {Buffer} from 'buffer';
12
12
  import {Executor} from "./Executor";
13
+ import { normalizeSingularRequest } from "../utils/normalizeSingularRequest";
13
14
 
14
15
  export class SqlExecutor<
15
16
  G extends OrmGenerics
@@ -19,10 +20,22 @@ export class SqlExecutor<
19
20
  const {TABLE_NAME} = this.config.restModel;
20
21
  const method = this.config.requestMethod;
21
22
 
23
+ // Normalize singular T-shaped requests into complex ORM shape (GET/PUT/DELETE)
24
+ try {
25
+ this.request = normalizeSingularRequest(
26
+ method as any,
27
+ this.request as any,
28
+ this.config.restModel as any,
29
+ undefined
30
+ ) as typeof this.request;
31
+ } catch (e) {
32
+ // Surface normalization errors early
33
+ throw e;
34
+ }
35
+
22
36
  this.config.verbose && console.log(`[SQL EXECUTOR] ▶️ Executing ${method} on table "${TABLE_NAME}"`);
23
37
  this.config.verbose && console.log(`[SQL EXECUTOR] 🧩 Request:`, this.request);
24
38
 
25
-
26
39
  switch (method) {
27
40
  case 'GET': {
28
41
  const rest = await this.runQuery();
@@ -2,6 +2,8 @@ import {Executor} from "../../executors/Executor";
2
2
  import {OrmGenerics} from "../../types/ormGenerics";
3
3
 
4
4
  export abstract class AggregateBuilder<G extends OrmGenerics> extends Executor<G>{
5
+ protected selectAliases: Set<string> = new Set<string>();
6
+
5
7
  buildAggregateField(field: string | any[]): string {
6
8
  if (typeof field === 'string') {
7
9
  return field;
@@ -33,6 +35,7 @@ export abstract class AggregateBuilder<G extends OrmGenerics> extends Executor<G
33
35
  }
34
36
 
35
37
  if (alias) {
38
+ this.selectAliases.add(alias);
36
39
  expr += ` AS ${alias}`;
37
40
  }
38
41
 
@@ -1,4 +1,4 @@
1
- import {C6C} from "api/C6Constants";
1
+ import {C6C} from "../../C6Constants";
2
2
  import {OrmGenerics} from "../../types/ormGenerics";
3
3
  import {DetermineResponseDataType} from "../../types/ormInterfaces";
4
4
  import {convertHexIfBinary, SqlBuilderResult} from "../utils/sqlUtils";
@@ -65,7 +65,16 @@ export abstract class ConditionBuilder<
65
65
  ]);
66
66
 
67
67
  private isTableReference(val: any): boolean {
68
- if (typeof val !== 'string' || !val.includes('.')) return false;
68
+ if (typeof val !== 'string') return false;
69
+ // Support aggregate aliases (e.g., SELECT COUNT(x) AS cnt ... HAVING cnt > 1)
70
+ if (!val.includes('.')) {
71
+ const isIdentifier = /^[A-Za-z_][A-Za-z0-9_]*$/.test(val);
72
+ // selectAliases is defined in AggregateBuilder
73
+ if (isIdentifier && (this as any).selectAliases?.has(val)) {
74
+ return true;
75
+ }
76
+ return false;
77
+ }
69
78
  const [prefix, column] = val.split('.');
70
79
  const tableName = this.aliasMap[prefix] ?? prefix;
71
80
  const table = this.config.C6?.TABLES?.[tableName];
@@ -90,7 +99,13 @@ export abstract class ConditionBuilder<
90
99
  column: string,
91
100
  value: any
92
101
  ): string {
93
- const columnDef = this.config.C6[column.split('.')[0]]?.TYPE_VALIDATION?.[column];
102
+ // Determine column definition from C6.TABLES to support type-aware conversions (e.g., BINARY hex -> Buffer)
103
+ let columnDef: any | undefined;
104
+ if (typeof column === 'string' && column.includes('.')) {
105
+ const [tableName] = column.split('.', 2);
106
+ const table = this.config.C6?.TABLES?.[tableName];
107
+ columnDef = table?.TYPE_VALIDATION?.[column];
108
+ }
94
109
  const val = convertHexIfBinary(column, value, columnDef);
95
110
 
96
111
  if (this.useNamedParams) {
@@ -141,7 +156,7 @@ export abstract class ConditionBuilder<
141
156
  const leftIsRef = this.isTableReference(column);
142
157
  const rightIsCol = typeof value === 'string' && this.isColumnRef(value);
143
158
 
144
- if (!leftIsCol && !rightIsCol) {
159
+ if (!leftIsCol && !leftIsRef && !rightIsCol) {
145
160
  throw new Error(`Potential SQL injection detected: '${column} ${op} ${value}'`);
146
161
  }
147
162
 
@@ -222,14 +237,11 @@ export abstract class ConditionBuilder<
222
237
 
223
238
  const buildFromObject = (obj: Record<string, any>, mode: boolean) => {
224
239
  const subParts: string[] = [];
225
- for (const [k, v] of Object.entries(obj)) {
226
- // numeric keys represent nested OR groups
227
- if (!isNaN(Number(k))) {
228
- const sub = this.buildBooleanJoinedConditions(v, false, params);
229
- if (sub) subParts.push(sub);
230
- continue;
231
- }
240
+ const entries = Object.entries(obj);
241
+ const nonNumeric = entries.filter(([k]) => isNaN(Number(k)));
242
+ const numeric = entries.filter(([k]) => !isNaN(Number(k)));
232
243
 
244
+ const processEntry = (k: string, v: any) => {
233
245
  if (typeof v === 'object' && v !== null && Object.keys(v).length === 1) {
234
246
  const [op, val] = Object.entries(v)[0];
235
247
  subParts.push(addCondition(k, op, val));
@@ -242,7 +254,18 @@ export abstract class ConditionBuilder<
242
254
  } else {
243
255
  subParts.push(addCondition(k, '=', v));
244
256
  }
257
+ };
258
+
259
+ // Process non-numeric keys first to preserve intuitive insertion order for params
260
+ for (const [k, v] of nonNumeric) {
261
+ processEntry(k, v);
245
262
  }
263
+ // Then process numeric keys (treated as grouped OR conditions)
264
+ for (const [_k, v] of numeric) {
265
+ const sub = this.buildBooleanJoinedConditions(v, false, params);
266
+ if (sub) subParts.push(sub);
267
+ }
268
+
246
269
  return subParts.join(` ${mode ? 'AND' : 'OR'} `);
247
270
  };
248
271
 
@@ -1,4 +1,4 @@
1
- import {C6Constants} from "api/C6Constants";
1
+ import {C6Constants} from "../../C6Constants";
2
2
  import {OrmGenerics} from "../../types/ormGenerics";
3
3
  import {JoinBuilder} from "./JoinBuilder";
4
4
 
@@ -44,9 +44,15 @@ export abstract class PaginationBuilder<G extends OrmGenerics> extends JoinBuild
44
44
  /* -------- LIMIT / OFFSET -------- */
45
45
  if (pagination?.[C6Constants.LIMIT] != null) {
46
46
  const lim = parseInt(pagination[C6Constants.LIMIT], 10);
47
- const page = parseInt(pagination[C6Constants.PAGE] ?? 1, 10);
48
- const offset = (page - 1) * lim;
49
- sql += ` LIMIT ${offset}, ${lim}`;
47
+ const pageRaw = pagination[C6Constants.PAGE];
48
+ const pageParsed = parseInt(pageRaw ?? 1, 10);
49
+ const page = isFinite(pageParsed) && pageParsed > 1 ? pageParsed : 1;
50
+ if (page === 1) {
51
+ sql += ` LIMIT ${lim}`;
52
+ } else {
53
+ const offset = (page - 1) * lim;
54
+ sql += ` LIMIT ${offset}, ${lim}`;
55
+ }
50
56
  }
51
57
 
52
58
  this.config.verbose && console.log(`[PAGINATION] ${sql.trim()}`);
@@ -9,6 +9,9 @@ export class SelectQueryBuilder<G extends OrmGenerics> extends PaginationBuilder
9
9
  isSubSelect: boolean = false
10
10
  ): SqlBuilderResult {
11
11
  this.aliasMap = {};
12
+ // reset any previously collected SELECT aliases (from AggregateBuilder)
13
+ // @ts-ignore
14
+ if (this.selectAliases && this.selectAliases.clear) this.selectAliases.clear();
12
15
  const args = this.request;
13
16
  this.initAlias(table, args.JOIN);
14
17
  const params = this.useNamedParams ? {} : [];
@@ -22,7 +22,8 @@ export class UpdateQueryBuilder<G extends OrmGenerics> extends PaginationBuilder
22
22
  throw new Error("No update data provided in the request.");
23
23
  }
24
24
 
25
- const setClauses = Object.entries(this.request[C6C.UPDATE]).map(([col, val]) => this.addParam(params, col, val));
25
+ const setClauses = Object.entries(this.request[C6C.UPDATE])
26
+ .map(([col, val]) => `\`${col}\` = ${this.addParam(params, col, val)}`);
26
27
 
27
28
  sql += ` SET ${setClauses.join(', ')}`;
28
29
 
@@ -66,7 +66,7 @@ export type Pagination<T = any> = {
66
66
  ORDER?: Partial<Record<keyof T, OrderDirection>>;
67
67
  };
68
68
 
69
- export type RequestGetPutDeleteBody<T extends { [key: string]: any } = any> = {
69
+ export type RequestGetPutDeleteBody<T extends { [key: string]: any } = any> = T | {
70
70
  SELECT?: SelectField<T>[];
71
71
  UPDATE?: Partial<T>;
72
72
  DELETE?: boolean;
@@ -84,6 +84,7 @@ export type iAPI<T extends { [key: string]: any }> = T & {
84
84
  error?: string | ((r: AxiosResponse) => string | void);
85
85
  };
86
86
 
87
+ // TODO - Eventually I believe we can support complex posts.
87
88
  export type RequestQueryBody<
88
89
  Method extends iRestMethods,
89
90
  T extends { [key: string]: any },
@@ -91,9 +92,7 @@ export type RequestQueryBody<
91
92
  Overrides extends { [key: string]: any } = {}
92
93
  > = Method extends 'GET' | 'PUT' | 'DELETE'
93
94
  ? iAPI<RequestGetPutDeleteBody<Modify<T, Overrides> & Custom>>
94
- : Method extends 'POST'
95
- ? iAPI<RequestGetPutDeleteBody<Modify<T, Overrides> & Custom> & Modify<T, Overrides> & Custom>
96
- : iAPI<Modify<T, Overrides> & Custom>;
95
+ : iAPI<Modify<T, Overrides> & Custom>;
97
96
 
98
97
  export interface iCacheAPI<ResponseDataType = any> {
99
98
  requestArgumentsSerialized: string;
@@ -1,5 +1,5 @@
1
1
  import {AxiosPromise} from "axios";
2
- import { iCacheAPI } from "api/types/ormInterfaces";
2
+ import { iCacheAPI } from "../types/ormInterfaces";
3
3
 
4
4
  // do not remove entries from this array. It is used to track the progress of API requests.
5
5
  // position in array is important. Do not sort. To not add to begging.