@carbonorm/carbonnode 6.0.19 → 6.1.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.
Files changed (89) hide show
  1. package/README.md +46 -1
  2. package/dist/constants/C6Constants.d.ts +342 -338
  3. package/dist/executors/SqlExecutor.d.ts +8 -0
  4. package/dist/index.cjs.js +751 -272
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.esm.js +744 -273
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/orm/builders/AggregateBuilder.d.ts +5 -1
  10. package/dist/orm/builders/ConditionBuilder.d.ts +2 -3
  11. package/dist/orm/builders/ExpressionSerializer.d.ts +22 -0
  12. package/dist/orm/builders/PaginationBuilder.d.ts +4 -6
  13. package/dist/orm/queryHelpers.d.ts +12 -1
  14. package/dist/types/mysqlTypes.d.ts +6 -1
  15. package/dist/types/ormInterfaces.d.ts +7 -5
  16. package/dist/utils/cacheManager.d.ts +3 -2
  17. package/package.json +2 -2
  18. package/scripts/assets/handlebars/C6.test.ts.handlebars +4 -4
  19. package/src/__tests__/cacheManager.test.ts +28 -0
  20. package/src/__tests__/expressServer.e2e.test.ts +26 -17
  21. package/src/__tests__/httpExecutorSingular.e2e.test.ts +53 -14
  22. package/src/__tests__/normalizeSingularRequest.test.ts +26 -8
  23. package/src/__tests__/sakila-db/C6.js +1 -1
  24. package/src/__tests__/sakila-db/C6.mysqldump.json +1 -1
  25. package/src/__tests__/sakila-db/C6.mysqldump.sql +1 -1
  26. package/src/__tests__/sakila-db/C6.sqlAllowList.json +1 -1
  27. package/src/__tests__/sakila-db/C6.test.ts +4 -4
  28. package/src/__tests__/sakila-db/C6.ts +1 -1
  29. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +11 -4
  30. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.latest.json +3 -3
  31. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.json +1 -1
  32. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.lookup.json +3 -3
  33. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.json +26 -7
  34. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.latest.json +5 -5
  35. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.json +1 -1
  36. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.lookup.json +5 -5
  37. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.json +9 -3
  38. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.latest.json +2 -2
  39. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.json +1 -1
  40. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.lookup.json +2 -2
  41. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.json +10 -3
  42. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.latest.json +2 -2
  43. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.json +1 -1
  44. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.lookup.json +2 -2
  45. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.json +9 -3
  46. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.latest.json +2 -2
  47. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.json +1 -1
  48. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.lookup.json +2 -2
  49. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.json +18 -6
  50. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.latest.json +5 -5
  51. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.json +1 -1
  52. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.lookup.json +5 -5
  53. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.json +18 -3
  54. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.latest.json +2 -2
  55. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.json +1 -1
  56. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.lookup.json +2 -2
  57. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.json +9 -2
  58. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.latest.json +1 -1
  59. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.json +1 -1
  60. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.lookup.json +1 -1
  61. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.json +9 -3
  62. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.latest.json +2 -2
  63. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.json +1 -1
  64. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.lookup.json +2 -2
  65. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.json +13 -3
  66. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.latest.json +2 -2
  67. package/src/__tests__/sakila-db/sqlResponses/C6.payment.put.lookup.json +2 -2
  68. package/src/__tests__/sakila-db/sqlResponses/C6.rental.join.json +10 -10
  69. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.json +14 -4
  70. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.latest.json +3 -3
  71. package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.json +1 -1
  72. package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.lookup.json +3 -3
  73. package/src/__tests__/sqlBuilders.complex.test.ts +62 -74
  74. package/src/__tests__/sqlBuilders.expressions.test.ts +58 -30
  75. package/src/__tests__/sqlBuilders.test.ts +68 -4
  76. package/src/__tests__/sqlExecutorPostUuid.test.ts +185 -0
  77. package/src/constants/C6Constants.ts +3 -1
  78. package/src/executors/HttpExecutor.ts +35 -6
  79. package/src/executors/SqlExecutor.ts +232 -4
  80. package/src/index.ts +1 -0
  81. package/src/orm/builders/AggregateBuilder.ts +67 -106
  82. package/src/orm/builders/ConditionBuilder.ts +69 -93
  83. package/src/orm/builders/ExpressionSerializer.ts +275 -0
  84. package/src/orm/builders/PaginationBuilder.ts +24 -34
  85. package/src/orm/queryHelpers.ts +29 -0
  86. package/src/types/mysqlTypes.ts +130 -9
  87. package/src/types/ormInterfaces.ts +7 -7
  88. package/src/utils/cacheManager.ts +6 -4
  89. package/src/utils/normalizeSingularRequest.ts +11 -4
@@ -30,22 +30,20 @@ describe('Explicit SQL expression grammar', () => {
30
30
  [C6C.OR]: [
31
31
  {
32
32
  [C6C.LESS_THAN_OR_EQUAL_TO]: [
33
- {
34
- [C6C.ST_DISTANCE_SPHERE]: [
35
- Property_Units.LOCATION,
36
- { [C6C.ST_GEOMFROMTEXT]: ['POINT(39.4972468 -105.0403593)', 4326] },
37
- ],
38
- },
33
+ [
34
+ C6C.ST_DISTANCE_SPHERE,
35
+ Property_Units.LOCATION,
36
+ [C6C.ST_GEOMFROMTEXT, [C6C.LIT, 'POINT(39.4972468 -105.0403593)'], 4326],
37
+ ],
39
38
  5000,
40
39
  ],
41
40
  },
42
41
  {
43
42
  [C6C.GREATER_THAN]: [
44
- {
45
- [C6C.ST_AREA]: [
46
- { [C6C.ST_GEOMFROMTEXT]: ['POLYGON((0 0,1 0,1 1,0 1,0 0))', 4326] },
47
- ],
48
- },
43
+ [
44
+ C6C.ST_AREA,
45
+ [C6C.ST_GEOMFROMTEXT, [C6C.LIT, 'POLYGON((0 0,1 0,1 1,0 1,0 0))'], 4326],
46
+ ],
49
47
  10000,
50
48
  ],
51
49
  },
@@ -56,12 +54,17 @@ describe('Explicit SQL expression grammar', () => {
56
54
  const { sql, params } = qb.build(Property_Units.TABLE_NAME);
57
55
 
58
56
  expect(sql).toContain('WHERE');
59
- expect(sql).toMatch(/ST_DISTANCE_SPHERE\(property_units.location, ST_GEOMFROMTEXT\('/);
60
- expect(sql).toMatch(/ST_AREA\(ST_GEOMFROMTEXT\('/);
57
+ expect(sql).toMatch(/ST_DISTANCE_SPHERE\(property_units.location, ST_GEOMFROMTEXT\(\?, 4326\)\)/);
58
+ expect(sql).toMatch(/ST_AREA\(ST_GEOMFROMTEXT\(\?, 4326\)\)/);
61
59
  expect(sql).toMatch(/<= \?/);
62
60
  expect(sql).toMatch(/> \?/);
63
61
  expect(sql).toContain('OR');
64
- expect(params).toEqual([5000, 10000]);
62
+ expect(params).toEqual([
63
+ 'POINT(39.4972468 -105.0403593)',
64
+ 5000,
65
+ 'POLYGON((0 0,1 0,1 1,0 1,0 0))',
66
+ 10000,
67
+ ]);
65
68
  });
66
69
 
67
70
  it('supports operator-first 3-tuple conditions in WHERE', () => {
@@ -85,7 +88,7 @@ describe('Explicit SQL expression grammar', () => {
85
88
  const qb = new SelectQueryBuilder(config as any, {
86
89
  [C6C.SELECT]: [Property_Units.PARCEL_ID],
87
90
  [C6C.WHERE]: {
88
- 0: [[C6C.BETWEEN, Parcel_Sales.SALE_DATE, ['2021-01-01', '2021-12-31']]],
91
+ 0: [[C6C.BETWEEN, Parcel_Sales.SALE_DATE, [[C6C.LIT, '2021-01-01'], [C6C.LIT, '2021-12-31']]]],
89
92
  },
90
93
  } as any, false);
91
94
 
@@ -94,30 +97,25 @@ describe('Explicit SQL expression grammar', () => {
94
97
  expect(params.slice(-2)).toEqual(['2021-01-01', '2021-12-31']);
95
98
  });
96
99
 
97
- it('treats safe raw function expressions as SQL expressions', () => {
100
+ it('supports tuple-based function expressions with literal wrappers', () => {
98
101
  const config = buildParcelConfig();
99
102
 
100
103
  const qb = new SelectQueryBuilder(config as any, {
101
104
  [C6C.SELECT]: [Property_Units.PARCEL_ID],
102
105
  [C6C.WHERE]: {
103
- 0: [
104
- [
105
- "ST_Distance_Sphere(property_units.location, ST_GeomFromText('POINT(39.4972468 -105.0403593)', 4326))",
106
- C6C.LESS_THAN_OR_EQUAL_TO,
107
- 5000,
108
- ],
106
+ [C6C.LESS_THAN_OR_EQUAL_TO]: [
107
+ [C6C.ST_DISTANCE_SPHERE, Property_Units.LOCATION, [C6C.ST_GEOMFROMTEXT, [C6C.LIT, 'POINT(39.4972468 -105.0403593)'], 4326]],
108
+ 5000,
109
109
  ],
110
110
  },
111
111
  } as any, false);
112
112
 
113
113
  const { sql, params } = qb.build(Property_Units.TABLE_NAME);
114
- expect(sql).toMatch(
115
- /ST_Distance_Sphere\(property_units\.location, ST_GeomFromText\('POINT\(39\.4972468 -105\.0403593\)', 4326\)\) <= \?/
116
- );
117
- expect(params.slice(-1)[0]).toBe(5000);
114
+ expect(sql).toMatch(/ST_DISTANCE_SPHERE\(property_units\.location, ST_GEOMFROMTEXT\(\?, 4326\)\) <= \?/);
115
+ expect(params).toEqual(['POINT(39.4972468 -105.0403593)', 5000]);
118
116
  });
119
117
 
120
- it('rejects raw function expressions containing unsafe tokens', () => {
118
+ it('rejects raw function expression strings', () => {
121
119
  const config = buildParcelConfig();
122
120
 
123
121
  const qb = new SelectQueryBuilder(config as any, {
@@ -133,7 +131,37 @@ describe('Explicit SQL expression grammar', () => {
133
131
  },
134
132
  } as any, false);
135
133
 
136
- expect(() => qb.build(Property_Units.TABLE_NAME)).toThrowError(/Potential SQL injection detected/);
134
+ expect(() => qb.build(Property_Units.TABLE_NAME)).toThrowError(/Bare string/);
135
+ });
136
+
137
+ it('rejects legacy positional AS syntax in function tuples', () => {
138
+ const config = buildParcelConfig();
139
+
140
+ const qb = new SelectQueryBuilder(config as any, {
141
+ [C6C.SELECT]: [[C6C.COUNT, Property_Units.PARCEL_ID, C6C.AS, 'cnt']],
142
+ } as any, false);
143
+
144
+ expect(() => qb.build(Property_Units.TABLE_NAME)).toThrowError(/Legacy positional AS syntax/);
145
+ });
146
+
147
+ it('rejects legacy positional AS syntax in column tuples', () => {
148
+ const config = buildParcelConfig();
149
+
150
+ const qb = new SelectQueryBuilder(config as any, {
151
+ [C6C.SELECT]: [[Property_Units.PARCEL_ID, C6C.AS, 'parcel_id_alias']],
152
+ } as any, false);
153
+
154
+ expect(() => qb.build(Property_Units.TABLE_NAME)).toThrowError(/Legacy positional AS syntax/);
155
+ });
156
+
157
+ it('rejects object-rooted function expressions', () => {
158
+ const config = buildParcelConfig();
159
+
160
+ const qb = new SelectQueryBuilder(config as any, {
161
+ [C6C.SELECT]: [{ [C6C.COUNT]: [Property_Units.PARCEL_ID] }],
162
+ } as any, false);
163
+
164
+ expect(() => qb.build(Property_Units.TABLE_NAME)).toThrowError(/Object-rooted expressions are not supported/);
137
165
  });
138
166
 
139
167
  it('supports explicit AND groupings composed of nested OR clauses', () => {
@@ -146,8 +174,8 @@ describe('Explicit SQL expression grammar', () => {
146
174
  { [C6C.GREATER_THAN]: [Property_Units.PARCEL_ID, 100] },
147
175
  {
148
176
  [C6C.OR]: [
149
- { [C6C.BETWEEN]: [Parcel_Sales.SALE_DATE, ['2021-01-01', '2022-06-30']] },
150
- { [C6C.BETWEEN]: [Parcel_Sales.SALE_DATE, ['2023-01-01', '2024-06-30']] },
177
+ { [C6C.BETWEEN]: [Parcel_Sales.SALE_DATE, [[C6C.LIT, '2021-01-01'], [C6C.LIT, '2022-06-30']]] },
178
+ { [C6C.BETWEEN]: [Parcel_Sales.SALE_DATE, [[C6C.LIT, '2023-01-01'], [C6C.LIT, '2024-06-30']]] },
151
179
  ],
152
180
  },
153
181
  ],
@@ -4,6 +4,7 @@ import { SelectQueryBuilder } from '../orm/queries/SelectQueryBuilder';
4
4
  import { PostQueryBuilder } from '../orm/queries/PostQueryBuilder';
5
5
  import { UpdateQueryBuilder } from '../orm/queries/UpdateQueryBuilder';
6
6
  import { DeleteQueryBuilder } from '../orm/queries/DeleteQueryBuilder';
7
+ import { alias, call, distinct, fn, lit, order } from '../orm/queryHelpers';
7
8
  import { buildTestConfig, buildBinaryTestConfig, buildBinaryTestConfigFqn } from './fixtures/c6.fixture';
8
9
 
9
10
  describe('SQL Builders', () => {
@@ -11,7 +12,7 @@ describe('SQL Builders', () => {
11
12
  const config = buildTestConfig();
12
13
  // named params disabled -> positional params array
13
14
  const qb = new SelectQueryBuilder(config as any, {
14
- SELECT: ['actor.first_name', [C6C.COUNT, 'actor.actor_id', C6C.AS, 'cnt']],
15
+ SELECT: ['actor.first_name', [C6C.AS, [C6C.COUNT, 'actor.actor_id'], 'cnt']],
15
16
  JOIN: {
16
17
  [C6C.INNER]: {
17
18
  'film_actor fa': {
@@ -20,7 +21,7 @@ describe('SQL Builders', () => {
20
21
  }
21
22
  },
22
23
  WHERE: {
23
- 'actor.first_name': [C6C.LIKE, '%A%'],
24
+ 'actor.first_name': [C6C.LIKE, [C6C.LIT, '%A%']],
24
25
  0: {
25
26
  'actor.actor_id': [C6C.GREATER_THAN, 10],
26
27
  }
@@ -47,7 +48,7 @@ describe('SQL Builders', () => {
47
48
  const config = buildTestConfig();
48
49
  const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
49
50
  const qb = new SelectQueryBuilder(config as any, {
50
- SELECT: [[C6C.COUNT, 'actor.actor_id', C6C.AS, 'cnt']],
51
+ SELECT: [[C6C.AS, [C6C.COUNT, 'actor.actor_id'], 'cnt']],
51
52
  } as any, false);
52
53
 
53
54
  qb.build('actor');
@@ -62,6 +63,39 @@ describe('SQL Builders', () => {
62
63
  logSpy.mockRestore();
63
64
  });
64
65
 
66
+ it('supports custom functions through C6C.CALL and binds string literals', () => {
67
+ const config = buildTestConfig();
68
+ const qb = new SelectQueryBuilder(config as any, {
69
+ SELECT: [[C6C.AS, [C6C.CALL, 'COALESCE', [C6C.LIT, 'fallback'], 'actor.first_name'], 'resolved_name']],
70
+ } as any, false);
71
+
72
+ const { sql, params } = qb.build('actor');
73
+ expect(sql).toContain('COALESCE(?, actor.first_name) AS resolved_name');
74
+ expect(params).toEqual(['fallback']);
75
+ });
76
+
77
+ it('supports helper builders for fn/call/as/distinct/lit/order', () => {
78
+ const config = buildTestConfig();
79
+ const qb = new SelectQueryBuilder(config as any, {
80
+ SELECT: [
81
+ alias(distinct('actor.first_name'), 'distinct_name'),
82
+ alias(fn(C6C.COUNT, 'actor.actor_id'), 'cnt'),
83
+ call('COALESCE', lit('N/A'), 'actor.last_name'),
84
+ ],
85
+ PAGINATION: {
86
+ [C6C.ORDER]: [order(fn(C6C.COUNT, 'actor.actor_id'), 'DESC')],
87
+ [C6C.LIMIT]: 5,
88
+ },
89
+ } as any, false);
90
+
91
+ const { sql, params } = qb.build('actor');
92
+ expect(sql).toContain('DISTINCT actor.first_name AS distinct_name');
93
+ expect(sql).toContain('COUNT(actor.actor_id) AS cnt');
94
+ expect(sql).toContain('COALESCE(?, actor.last_name)');
95
+ expect(sql).toContain('ORDER BY COUNT(actor.actor_id) DESC');
96
+ expect(params).toEqual(['N/A']);
97
+ });
98
+
65
99
  it('builds INSERT with ON DUPLICATE KEY UPDATE', () => {
66
100
  const config = buildTestConfig();
67
101
  const qb = new PostQueryBuilder(config as any, {
@@ -192,6 +226,36 @@ describe('SQL Builders', () => {
192
226
  expect(params).toEqual(['ALICE', 5]);
193
227
  });
194
228
 
229
+ it('supports expression tuples in UPDATE values', () => {
230
+ const config = buildTestConfig();
231
+ const qb = new UpdateQueryBuilder(config as any, {
232
+ [C6C.UPDATE]: {
233
+ 'actor.first_name': [C6C.CONCAT, [C6C.LIT, 'Mr. '], 'actor.last_name'],
234
+ },
235
+ WHERE: {
236
+ 'actor.actor_id': [C6C.EQUAL, 7],
237
+ },
238
+ } as any, false);
239
+
240
+ const { sql, params } = qb.build('actor');
241
+ expect(sql).toContain('`first_name` = CONCAT(?, actor.last_name)');
242
+ expect(params).toEqual(['Mr. ', 7]);
243
+ });
244
+
245
+ it('supports expression tuples in INSERT values', () => {
246
+ const config = buildTestConfig();
247
+ const qb = new PostQueryBuilder(config as any, {
248
+ [C6C.INSERT]: {
249
+ 'actor.first_name': [C6C.CONCAT, [C6C.LIT, 'HEL'], [C6C.LIT, 'LO']],
250
+ 'actor.last_name': 'SMITH',
251
+ },
252
+ } as any, false);
253
+
254
+ const { sql, params } = qb.build('actor');
255
+ expect(sql).toContain('CONCAT(?, ?)');
256
+ expect(params).toEqual(['HEL', 'LO', 'SMITH']);
257
+ });
258
+
195
259
  it('builds UPDATE when columns are fully qualified', () => {
196
260
  const config = buildTestConfig();
197
261
  const qb = new UpdateQueryBuilder(config as any, {
@@ -235,7 +299,7 @@ describe('SQL Builders', () => {
235
299
  const config = buildTestConfig();
236
300
  const qb = new SelectQueryBuilder(config as any, {
237
301
  WHERE: {
238
- 'actor.binarycol': [C6C.EQUAL, '0123456789abcdef0123456789abcdef']
302
+ 'actor.binarycol': [C6C.EQUAL, [C6C.LIT, '0123456789abcdef0123456789abcdef']]
239
303
  }
240
304
  } as any, false);
241
305
 
@@ -0,0 +1,185 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { SqlExecutor } from "../executors/SqlExecutor";
3
+
4
+ function buildLifecycleHooks() {
5
+ return { GET: {}, POST: {}, PUT: {}, DELETE: {} } as any;
6
+ }
7
+
8
+ function buildUuidPrimaryConfig(conn: any) {
9
+ const restModel: any = {
10
+ TABLE_NAME: "report_dashboards",
11
+ PRIMARY: ["report_dashboards.dashboard_id"],
12
+ PRIMARY_SHORT: ["dashboard_id"],
13
+ COLUMNS: {
14
+ "report_dashboards.dashboard_id": "dashboard_id",
15
+ "report_dashboards.title": "title",
16
+ "report_dashboards.created_by": "created_by",
17
+ },
18
+ TYPE_VALIDATION: {
19
+ "report_dashboards.dashboard_id": {
20
+ MYSQL_TYPE: "binary",
21
+ MAX_LENGTH: "16",
22
+ AUTO_INCREMENT: false,
23
+ SKIP_COLUMN_IN_POST: false,
24
+ },
25
+ "report_dashboards.title": {
26
+ MYSQL_TYPE: "varchar",
27
+ MAX_LENGTH: "120",
28
+ AUTO_INCREMENT: false,
29
+ SKIP_COLUMN_IN_POST: false,
30
+ },
31
+ "report_dashboards.created_by": {
32
+ MYSQL_TYPE: "varchar",
33
+ MAX_LENGTH: "255",
34
+ AUTO_INCREMENT: false,
35
+ SKIP_COLUMN_IN_POST: false,
36
+ },
37
+ },
38
+ LIFECYCLE_HOOKS: buildLifecycleHooks(),
39
+ };
40
+
41
+ return {
42
+ requestMethod: "POST",
43
+ mysqlPool: {
44
+ getConnection: vi.fn(async () => conn),
45
+ },
46
+ C6: {
47
+ PREFIX: "",
48
+ TABLES: {
49
+ report_dashboards: restModel,
50
+ },
51
+ },
52
+ restModel,
53
+ } as any;
54
+ }
55
+
56
+ function buildAutoIncrementConfig(conn: any) {
57
+ const restModel: any = {
58
+ TABLE_NAME: "widgets",
59
+ PRIMARY: ["widgets.widget_id"],
60
+ PRIMARY_SHORT: ["widget_id"],
61
+ COLUMNS: {
62
+ "widgets.widget_id": "widget_id",
63
+ "widgets.name": "name",
64
+ },
65
+ TYPE_VALIDATION: {
66
+ "widgets.widget_id": {
67
+ MYSQL_TYPE: "int",
68
+ MAX_LENGTH: "11",
69
+ AUTO_INCREMENT: true,
70
+ SKIP_COLUMN_IN_POST: false,
71
+ },
72
+ "widgets.name": {
73
+ MYSQL_TYPE: "varchar",
74
+ MAX_LENGTH: "120",
75
+ AUTO_INCREMENT: false,
76
+ SKIP_COLUMN_IN_POST: false,
77
+ },
78
+ },
79
+ LIFECYCLE_HOOKS: buildLifecycleHooks(),
80
+ };
81
+
82
+ return {
83
+ requestMethod: "POST",
84
+ mysqlPool: {
85
+ getConnection: vi.fn(async () => conn),
86
+ },
87
+ C6: {
88
+ PREFIX: "",
89
+ TABLES: {
90
+ widgets: restModel,
91
+ },
92
+ },
93
+ restModel,
94
+ } as any;
95
+ }
96
+
97
+ function buildWriteConn(affectedRows = 1, insertId = 0) {
98
+ return {
99
+ beginTransaction: vi.fn(async () => undefined),
100
+ query: vi.fn(async () => [{ affectedRows, insertId }, []]),
101
+ commit: vi.fn(async () => undefined),
102
+ rollback: vi.fn(async () => undefined),
103
+ release: vi.fn(),
104
+ };
105
+ }
106
+
107
+ function expectUuidV7Hex(value: unknown) {
108
+ expect(typeof value).toBe("string");
109
+ const hex = String(value).toUpperCase();
110
+ expect(hex).toMatch(/^[0-9A-F]{32}$/);
111
+ expect(hex[12]).toBe("7");
112
+ expect(hex[16]).toMatch(/[89AB]/);
113
+ }
114
+
115
+ describe("SqlExecutor POST UUID synthesis", () => {
116
+ it("generates missing UUID primary keys for POST and populates rest payload", async () => {
117
+ const conn = buildWriteConn(1, 0);
118
+ const config = buildUuidPrimaryConfig(conn);
119
+ const request: any = {
120
+ title: "Board One",
121
+ created_by: "user-1",
122
+ };
123
+
124
+ const executor = new SqlExecutor<any>(config, request);
125
+ const result: any = await executor.execute();
126
+
127
+ expect(Array.isArray(result.rest)).toBe(true);
128
+ expect(result.rest).toHaveLength(1);
129
+ expect(result.rest[0]).toMatchObject({
130
+ title: "Board One",
131
+ created_by: "user-1",
132
+ });
133
+ expectUuidV7Hex(result.rest[0].dashboard_id);
134
+
135
+ const [sql, values] = conn.query.mock.calls[0];
136
+ expect(sql).toContain("INSERT INTO `report_dashboards`");
137
+ const uuidBuffers = (values as any[]).filter((value) => Buffer.isBuffer(value));
138
+ expect(uuidBuffers).toHaveLength(1);
139
+ expect(uuidBuffers[0]).toHaveLength(16);
140
+ });
141
+
142
+ it("generates UUIDs for each multi-row insert payload row", async () => {
143
+ const conn = buildWriteConn(2, 0);
144
+ const config = buildUuidPrimaryConfig(conn);
145
+ const request: any = {
146
+ dataInsertMultipleRows: [
147
+ { title: "One", created_by: "user-1" },
148
+ { title: "Two", created_by: "user-1" },
149
+ ],
150
+ };
151
+
152
+ const executor = new SqlExecutor<any>(config, request);
153
+ const result: any = await executor.execute();
154
+
155
+ expect(Array.isArray(result.rest)).toBe(true);
156
+ expect(result.rest).toHaveLength(2);
157
+ expectUuidV7Hex(result.rest[0].dashboard_id);
158
+ expectUuidV7Hex(result.rest[1].dashboard_id);
159
+ expect(result.rest[0].dashboard_id).not.toBe(result.rest[1].dashboard_id);
160
+
161
+ const [, values] = conn.query.mock.calls[0];
162
+ const uuidBuffers = (values as any[]).filter((value) => Buffer.isBuffer(value));
163
+ expect(uuidBuffers).toHaveLength(2);
164
+ expect(uuidBuffers[0]).toHaveLength(16);
165
+ expect(uuidBuffers[1]).toHaveLength(16);
166
+ });
167
+
168
+ it("fills response rest primary key from insertId for autoincrement inserts", async () => {
169
+ const conn = buildWriteConn(1, 42);
170
+ const config = buildAutoIncrementConfig(conn);
171
+ const request: any = {
172
+ name: "Auto Row",
173
+ };
174
+
175
+ const executor = new SqlExecutor<any>(config, request);
176
+ const result: any = await executor.execute();
177
+
178
+ expect(Array.isArray(result.rest)).toBe(true);
179
+ expect(result.rest).toHaveLength(1);
180
+ expect(result.rest[0]).toMatchObject({
181
+ widget_id: 42,
182
+ name: "Auto Row",
183
+ });
184
+ });
185
+ });
@@ -9,6 +9,7 @@ export const C6Constants = {
9
9
  BETWEEN: 'BETWEEN',
10
10
 
11
11
  CONCAT: 'CONCAT',
12
+ CALL: 'CALL',
12
13
  CONVERT_TZ: 'CONVERT_TZ',
13
14
  COUNT: 'COUNT',
14
15
  COUNT_ALL: 'COUNT_ALL',
@@ -71,6 +72,7 @@ export const C6Constants = {
71
72
  LESS_THAN: '<',
72
73
  LESS_THAN_OR_EQUAL_TO: '<=',
73
74
  LIKE: 'LIKE',
75
+ LIT: 'LIT',
74
76
  LIMIT: 'LIMIT',
75
77
  LOCALTIME: 'LOCALTIME',
76
78
  LOCALTIMESTAMP: 'LOCALTIMESTAMP',
@@ -198,7 +200,7 @@ export const C6Constants = {
198
200
  FINISH: 'FINISH',
199
201
  VALIDATE_C6_ENTITY_ID_REGEX: '#^([a-fA-F0-9]{20,35})$#',
200
202
 
201
- };
203
+ } as const;
202
204
 
203
205
 
204
206
  export const C6C = C6Constants;
@@ -14,6 +14,7 @@ import {
14
14
  } from "../types/ormInterfaces";
15
15
  import {removeInvalidKeys, removePrefixIfExists, TestRestfulResponse} from "../utils/apiHelpers";
16
16
  import {checkCache, evictCacheEntry, setCache, userCustomClearCache} from "../utils/cacheManager";
17
+ import type { SqlAllowListStatus } from "../utils/logSql";
17
18
  import {sortAndSerializeQueryObject} from "../utils/sortAndSerializeQueryObject";
18
19
  import {notifyToast} from "../utils/toastRuntime";
19
20
  import {Executor} from "./Executor";
@@ -81,11 +82,18 @@ export class HttpExecutor<
81
82
  ) {
82
83
  type RT = G['RestTableInterface'];
83
84
  type PK = G['PrimaryKey'];
85
+ const responseRestRaw = (response.data as any)?.rest;
86
+ const responseRows: Record<string, any>[] = Array.isArray(responseRestRaw)
87
+ ? responseRestRaw
88
+ : (responseRestRaw ? [responseRestRaw] : []);
84
89
 
85
90
  if (this.config.restModel.PRIMARY_SHORT.length === 1) {
86
91
  const pk = this.config.restModel.PRIMARY_SHORT[0] as PK;
87
92
  try {
88
- (request as unknown as Record<PK, RT[PK]>)[pk] = (response.data as any)?.created as RT[PK];
93
+ const created = (response.data as any)?.created ?? responseRows[0]?.[pk as string];
94
+ if (created !== undefined) {
95
+ (request as unknown as Record<PK, RT[PK]>)[pk] = created as RT[PK];
96
+ }
89
97
  } catch {/* best-effort */}
90
98
  } else if (isLocal()) {
91
99
  logWithLevel(
@@ -103,13 +111,13 @@ export class HttpExecutor<
103
111
  const normalizedRow = this.stripTableNameFromKeys<RT>(row as Partial<RT>);
104
112
  return removeInvalidKeys<RT>({
105
113
  ...normalizedRow,
106
- ...(index === 0 ? (response?.data as any)?.rest : {}),
114
+ ...(responseRows[index] ?? {}),
107
115
  }, this.config.C6.TABLES)
108
116
  })
109
117
  : [
110
118
  removeInvalidKeys<RT>({
111
119
  ...this.stripTableNameFromKeys<RT>(request as unknown as Partial<RT>),
112
- ...(response?.data as any)?.rest,
120
+ ...(responseRows[0] ?? {}),
113
121
  }, this.config.C6.TABLES)
114
122
  ],
115
123
  stateKey: this.config.restModel.TABLE_NAME,
@@ -201,6 +209,7 @@ export class HttpExecutor<
201
209
  const {
202
210
  debug,
203
211
  cacheResults = (C6.GET === requestMethod),
212
+ skipReactBootstrap = false,
204
213
  dataInsertMultipleRows,
205
214
  success,
206
215
  fetchDependencies = eFetchDependencies.NONE,
@@ -253,10 +262,19 @@ export class HttpExecutor<
253
262
  G['CustomAndRequiredFields'],
254
263
  G['RequestTableOverrides']
255
264
  >;
265
+ const cacheAllowListStatus: SqlAllowListStatus = this.config.sqlAllowListPath
266
+ ? "allowed"
267
+ : "not verified";
256
268
 
257
269
  const evictFromCache =
258
270
  requestMethod === GET && cacheResults
259
- ? () => evictCacheEntry(requestMethod, tableName, cacheRequestData, logContext)
271
+ ? () => evictCacheEntry(
272
+ requestMethod,
273
+ tableName,
274
+ cacheRequestData,
275
+ logContext,
276
+ cacheAllowListStatus,
277
+ )
260
278
  : undefined;
261
279
 
262
280
  // literally impossible for query to be undefined or null here but the editor is too busy licking windows to understand that
@@ -265,7 +283,13 @@ export class HttpExecutor<
265
283
  let cachedRequest: Promise<{ data: ResponseDataType }> | false = false;
266
284
 
267
285
  if (cacheResults) {
268
- cachedRequest = checkCache<ResponseDataType>(requestMethod, tableName, cacheRequestData, logContext);
286
+ cachedRequest = checkCache<ResponseDataType>(
287
+ requestMethod,
288
+ tableName,
289
+ cacheRequestData,
290
+ logContext,
291
+ cacheAllowListStatus,
292
+ );
269
293
  }
270
294
 
271
295
  if (cachedRequest) {
@@ -464,6 +488,7 @@ export class HttpExecutor<
464
488
  setCache<ResponseDataType>(requestMethod, tableName, cacheRequestData, {
465
489
  requestArgumentsSerialized: querySerialized,
466
490
  request: axiosActiveRequest,
491
+ allowListStatus: cacheAllowListStatus,
467
492
  });
468
493
  }
469
494
 
@@ -480,6 +505,7 @@ export class HttpExecutor<
480
505
  setCache<ResponseDataType>(requestMethod, tableName, cacheRequestData, {
481
506
  requestArgumentsSerialized: querySerialized,
482
507
  request: axiosActiveRequest,
508
+ allowListStatus: cacheAllowListStatus,
483
509
  response,
484
510
  final: true,
485
511
  });
@@ -503,6 +529,7 @@ export class HttpExecutor<
503
529
  setCache<ResponseDataType>(requestMethod, tableName, cacheRequestData, {
504
530
  requestArgumentsSerialized: querySerialized,
505
531
  request: axiosActiveRequest,
532
+ allowListStatus: cacheAllowListStatus,
506
533
  response,
507
534
  });
508
535
  }
@@ -531,7 +558,7 @@ export class HttpExecutor<
531
558
  response
532
559
  });
533
560
 
534
- if (undefined !== reactBootstrap && response) {
561
+ if (undefined !== reactBootstrap && response && !skipReactBootstrap) {
535
562
  switch (requestMethod) {
536
563
  case GET:
537
564
  response.data && reactBootstrap.updateRestfulObjectArrays<G['RestTableInterface']>({
@@ -582,6 +609,7 @@ export class HttpExecutor<
582
609
  setCache<ResponseDataType>(requestMethod, tableName, cacheRequestData, {
583
610
  requestArgumentsSerialized: querySerialized,
584
611
  request: axiosActiveRequest,
612
+ allowListStatus: cacheAllowListStatus,
585
613
  response,
586
614
  final: !hasNext,
587
615
  });
@@ -870,6 +898,7 @@ export class HttpExecutor<
870
898
  setCache<ResponseDataType>(requestMethod, tableName, cacheRequestData, {
871
899
  requestArgumentsSerialized: querySerialized,
872
900
  request: axiosActiveRequest,
901
+ allowListStatus: cacheAllowListStatus,
873
902
  response,
874
903
  final: true,
875
904
  });