@carbonorm/carbonnode 4.0.1 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +158 -49
  2. package/dist/api/executors/SqlExecutor.d.ts +6 -0
  3. package/dist/api/handlers/ExpressHandler.d.ts +2 -1
  4. package/dist/api/types/ormInterfaces.d.ts +12 -0
  5. package/dist/api/utils/sqlAllowList.d.ts +2 -0
  6. package/dist/index.cjs.js +247 -10
  7. package/dist/index.cjs.js.map +1 -1
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.esm.js +246 -11
  10. package/dist/index.esm.js.map +1 -1
  11. package/package.json +1 -1
  12. package/scripts/assets/handlebars/C6.test.ts.handlebars +578 -32
  13. package/scripts/generateRestBindings.cjs +5 -5
  14. package/scripts/generateRestBindings.ts +5 -5
  15. package/src/__tests__/fixtures/createTestServer.ts +11 -3
  16. package/src/__tests__/fixtures/sqlResponses/actor.get.json +13 -0
  17. package/src/__tests__/fixtures/sqlResponses/sqlAllowList.blocked.json +3 -0
  18. package/src/__tests__/fixtures/sqlResponses/sqlAllowList.json +3 -0
  19. package/src/__tests__/sakila-db/C6.js +1 -1
  20. package/src/__tests__/sakila-db/C6.mysql.cnf +6 -0
  21. package/src/__tests__/sakila-db/C6.mysqldump.json +1 -0
  22. package/src/__tests__/sakila-db/C6.mysqldump.sql +720 -0
  23. package/src/__tests__/sakila-db/C6.sqlAllowList.json +94 -0
  24. package/src/__tests__/sakila-db/C6.test.ts +578 -32
  25. package/src/__tests__/sakila-db/C6.ts +1 -1
  26. package/src/__tests__/sakila-db/sqlResponses/C6.actor.delete.json +10 -0
  27. package/src/__tests__/sakila-db/sqlResponses/C6.actor.delete.lookup.json +9 -0
  28. package/src/__tests__/sakila-db/sqlResponses/C6.actor.get.json +14 -0
  29. package/src/__tests__/sakila-db/sqlResponses/C6.actor.join.json +15 -0
  30. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +12 -0
  31. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.latest.json +14 -0
  32. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.json +11 -0
  33. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.lookup.json +16 -0
  34. package/src/__tests__/sakila-db/sqlResponses/C6.actor.seed.json +14 -0
  35. package/src/__tests__/sakila-db/sqlResponses/C6.address.delete.json +10 -0
  36. package/src/__tests__/sakila-db/sqlResponses/C6.address.delete.lookup.json +9 -0
  37. package/src/__tests__/sakila-db/sqlResponses/C6.address.fk.current.json +358 -0
  38. package/src/__tests__/sakila-db/sqlResponses/C6.address.fk.referenced.json +158 -0
  39. package/src/__tests__/sakila-db/sqlResponses/C6.address.get.json +22 -0
  40. package/src/__tests__/sakila-db/sqlResponses/C6.address.join.json +24 -0
  41. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.json +16 -0
  42. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.latest.json +22 -0
  43. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.json +11 -0
  44. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.lookup.json +24 -0
  45. package/src/__tests__/sakila-db/sqlResponses/C6.address.seed.json +22 -0
  46. package/src/__tests__/sakila-db/sqlResponses/C6.category.delete.json +10 -0
  47. package/src/__tests__/sakila-db/sqlResponses/C6.category.delete.lookup.json +9 -0
  48. package/src/__tests__/sakila-db/sqlResponses/C6.category.get.json +13 -0
  49. package/src/__tests__/sakila-db/sqlResponses/C6.category.join.json +14 -0
  50. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.json +11 -0
  51. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.latest.json +13 -0
  52. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.json +11 -0
  53. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.lookup.json +15 -0
  54. package/src/__tests__/sakila-db/sqlResponses/C6.category.seed.json +13 -0
  55. package/src/__tests__/sakila-db/sqlResponses/C6.city.delete.json +10 -0
  56. package/src/__tests__/sakila-db/sqlResponses/C6.city.delete.lookup.json +9 -0
  57. package/src/__tests__/sakila-db/sqlResponses/C6.city.fk.current.json +158 -0
  58. package/src/__tests__/sakila-db/sqlResponses/C6.city.fk.referenced.json +133 -0
  59. package/src/__tests__/sakila-db/sqlResponses/C6.city.get.json +14 -0
  60. package/src/__tests__/sakila-db/sqlResponses/C6.city.join.json +15 -0
  61. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.json +12 -0
  62. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.latest.json +14 -0
  63. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.json +11 -0
  64. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.lookup.json +16 -0
  65. package/src/__tests__/sakila-db/sqlResponses/C6.city.seed.json +14 -0
  66. package/src/__tests__/sakila-db/sqlResponses/C6.country.delete.json +10 -0
  67. package/src/__tests__/sakila-db/sqlResponses/C6.country.delete.lookup.json +9 -0
  68. package/src/__tests__/sakila-db/sqlResponses/C6.country.get.json +13 -0
  69. package/src/__tests__/sakila-db/sqlResponses/C6.country.join.json +15 -0
  70. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.json +11 -0
  71. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.latest.json +13 -0
  72. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.json +11 -0
  73. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.lookup.json +15 -0
  74. package/src/__tests__/sakila-db/sqlResponses/C6.country.seed.json +13 -0
  75. package/src/__tests__/sakila-db/sqlResponses/C6.customer.delete.json +10 -0
  76. package/src/__tests__/sakila-db/sqlResponses/C6.customer.delete.lookup.json +9 -0
  77. package/src/__tests__/sakila-db/sqlResponses/C6.customer.fk.current.json +283 -0
  78. package/src/__tests__/sakila-db/sqlResponses/C6.customer.fk.referenced.json +358 -0
  79. package/src/__tests__/sakila-db/sqlResponses/C6.customer.get.json +19 -0
  80. package/src/__tests__/sakila-db/sqlResponses/C6.customer.join.json +29 -0
  81. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.json +17 -0
  82. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.latest.json +19 -0
  83. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.json +11 -0
  84. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.lookup.json +21 -0
  85. package/src/__tests__/sakila-db/sqlResponses/C6.customer.seed.json +19 -0
  86. package/src/__tests__/sakila-db/sqlResponses/C6.film.delete.json +10 -0
  87. package/src/__tests__/sakila-db/sqlResponses/C6.film.delete.lookup.json +9 -0
  88. package/src/__tests__/sakila-db/sqlResponses/C6.film.fk.current.json +383 -0
  89. package/src/__tests__/sakila-db/sqlResponses/C6.film.fk.referenced.json +38 -0
  90. package/src/__tests__/sakila-db/sqlResponses/C6.film.get.json +23 -0
  91. package/src/__tests__/sakila-db/sqlResponses/C6.film.join.json +24 -0
  92. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.json +20 -0
  93. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.latest.json +23 -0
  94. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.json +11 -0
  95. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.lookup.json +25 -0
  96. package/src/__tests__/sakila-db/sqlResponses/C6.film.seed.json +23 -0
  97. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.delete.json +10 -0
  98. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.delete.lookup.json +9 -0
  99. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.fk.current.json +158 -0
  100. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.fk.referenced.json +20 -0
  101. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.get.json +14 -0
  102. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.join.json +25 -0
  103. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.json +12 -0
  104. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.latest.json +14 -0
  105. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.json +11 -0
  106. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.lookup.json +16 -0
  107. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.seed.json +14 -0
  108. package/src/__tests__/sakila-db/sqlResponses/C6.language.delete.json +10 -0
  109. package/src/__tests__/sakila-db/sqlResponses/C6.language.delete.lookup.json +9 -0
  110. package/src/__tests__/sakila-db/sqlResponses/C6.language.get.json +13 -0
  111. package/src/__tests__/sakila-db/sqlResponses/C6.language.join.json +24 -0
  112. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.json +11 -0
  113. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.latest.json +13 -0
  114. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.json +11 -0
  115. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.lookup.json +15 -0
  116. package/src/__tests__/sakila-db/sqlResponses/C6.language.seed.json +13 -0
  117. package/src/__tests__/sakila-db/sqlResponses/C6.payment.delete.json +10 -0
  118. package/src/__tests__/sakila-db/sqlResponses/C6.payment.delete.lookup.json +9 -0
  119. package/src/__tests__/sakila-db/sqlResponses/C6.payment.fk.current.json +233 -0
  120. package/src/__tests__/sakila-db/sqlResponses/C6.payment.fk.referenced.json +233 -0
  121. package/src/__tests__/sakila-db/sqlResponses/C6.payment.get.json +17 -0
  122. package/src/__tests__/sakila-db/sqlResponses/C6.payment.join.json +24 -0
  123. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.json +15 -0
  124. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.latest.json +17 -0
  125. package/src/__tests__/sakila-db/sqlResponses/C6.payment.put.json +11 -0
  126. package/src/__tests__/sakila-db/sqlResponses/C6.payment.put.lookup.json +19 -0
  127. package/src/__tests__/sakila-db/sqlResponses/C6.payment.seed.json +17 -0
  128. package/src/__tests__/sakila-db/sqlResponses/C6.rental.delete.json +10 -0
  129. package/src/__tests__/sakila-db/sqlResponses/C6.rental.delete.lookup.json +9 -0
  130. package/src/__tests__/sakila-db/sqlResponses/C6.rental.fk.current.json +233 -0
  131. package/src/__tests__/sakila-db/sqlResponses/C6.rental.fk.referenced.json +34 -0
  132. package/src/__tests__/sakila-db/sqlResponses/C6.rental.get.json +17 -0
  133. package/src/__tests__/sakila-db/sqlResponses/C6.rental.join.json +24 -0
  134. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.json +15 -0
  135. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.latest.json +17 -0
  136. package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.json +11 -0
  137. package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.lookup.json +19 -0
  138. package/src/__tests__/sakila-db/sqlResponses/C6.rental.seed.json +17 -0
  139. package/src/__tests__/sakila-db/sqlResponses/C6.staff.fk.current.json +34 -0
  140. package/src/__tests__/sakila-db/sqlResponses/C6.staff.fk.referenced.json +20 -0
  141. package/src/__tests__/sakila-db/sqlResponses/C6.staff.get.json +21 -0
  142. package/src/__tests__/sakila-db/sqlResponses/C6.staff.join.json +31 -0
  143. package/src/__tests__/sakila-db/sqlResponses/C6.staff.seed.json +21 -0
  144. package/src/__tests__/sakila-db/sqlResponses/C6.store.fk.current.json +20 -0
  145. package/src/__tests__/sakila-db/sqlResponses/C6.store.fk.referenced.json +34 -0
  146. package/src/__tests__/sakila-db/sqlResponses/C6.store.get.json +14 -0
  147. package/src/__tests__/sakila-db/sqlResponses/C6.store.join.json +24 -0
  148. package/src/__tests__/sakila-db/sqlResponses/C6.store.seed.json +14 -0
  149. package/src/__tests__/sakila.generated.test.ts +31 -0
  150. package/src/__tests__/sqlAllowList.test.ts +135 -0
  151. package/src/api/executors/SqlExecutor.ts +156 -0
  152. package/src/api/handlers/ExpressHandler.ts +10 -1
  153. package/src/api/types/ormInterfaces.ts +15 -0
  154. package/src/api/utils/sqlAllowList.ts +54 -0
  155. package/src/index.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carbonorm/carbonnode",
3
- "version": "4.0.1",
3
+ "version": "5.0.0",
4
4
  "browser": "dist/index.umd.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,6 +1,12 @@
1
1
  import mysql from 'mysql2/promise';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { mkdir, writeFile } from 'node:fs/promises';
2
5
  import {
3
6
  checkAllRequestsComplete,
7
+ normalizeSql,
8
+ type DetermineResponseDataType,
9
+ type OrmGenerics,
4
10
  } from '@carbonorm/carbonnode';
5
11
  import {
6
12
  C6,
@@ -14,10 +20,139 @@ import {
14
20
  afterAll,
15
21
  } from 'vitest';
16
22
 
17
- function toPascalCase(name) {
23
+ function toPascalCase(name: string) {
18
24
  return name.replace(/(^|_)([a-z])/g, (_, __, c) => c.toUpperCase());
19
25
  }
20
26
 
27
+ function stripPrefix(name: string) {
28
+ if (!C6.PREFIX) return name;
29
+ return name.startsWith(C6.PREFIX) ? name.slice(C6.PREFIX.length) : name;
30
+ }
31
+
32
+ function getBinding(shortName: string) {
33
+ return C6.ORM?.[toPascalCase(shortName)] ?? C6[toPascalCase(shortName)];
34
+ }
35
+
36
+ function formatTimestamp() {
37
+ return new Date().toISOString().slice(0, 19).replace('T', ' ');
38
+ }
39
+
40
+ type RestResponse<G extends OrmGenerics = OrmGenerics> = DetermineResponseDataType<
41
+ G['RequestMethod'],
42
+ G['RestTableInterface']
43
+ >;
44
+
45
+ type RestPromise<G extends OrmGenerics = OrmGenerics> = Promise<RestResponse<G>>;
46
+
47
+ function unwrapResponse<G extends OrmGenerics = OrmGenerics>(
48
+ response: RestResponse<G> | null | undefined
49
+ ) {
50
+ return response;
51
+ }
52
+
53
+ const __filename = fileURLToPath(import.meta.url);
54
+ const __dirname = path.dirname(__filename);
55
+ const sqlResponsesDir = path.join(__dirname, 'sqlResponses');
56
+ const sqlAllowListPath = path.join(__dirname, 'C6.sqlAllowList.json');
57
+ const sqlAllowListEntries = new Set<string>();
58
+
59
+ async function recordSqlResponse<G extends OrmGenerics = OrmGenerics>(
60
+ label: string,
61
+ response: RestResponse<G> | null | undefined
62
+ ) {
63
+ if (!response) return;
64
+ const payload = unwrapResponse(response);
65
+ if (!payload) return;
66
+ const sqlValue = payload?.sql?.sql ?? (typeof payload?.sql === 'string' ? payload.sql : undefined);
67
+ if (typeof sqlValue === 'string') {
68
+ sqlAllowListEntries.add(normalizeSql(sqlValue));
69
+ }
70
+
71
+ await mkdir(sqlResponsesDir, { recursive: true });
72
+ const filePath = path.join(sqlResponsesDir, `C6.${label}.json`);
73
+ await writeFile(filePath, JSON.stringify(payload, null, 2));
74
+ }
75
+
76
+ async function finalizeSqlAllowList() {
77
+ await mkdir(sqlResponsesDir, { recursive: true });
78
+ const compiled = Array.from(sqlAllowListEntries).sort();
79
+ await writeFile(sqlAllowListPath, JSON.stringify(compiled, null, 2));
80
+ }
81
+
82
+ async function executeAndRecord<G extends OrmGenerics = OrmGenerics>(
83
+ label: string,
84
+ fn: () => RestPromise<G>
85
+ ) {
86
+ const result = await fn();
87
+ await recordSqlResponse<G>(label, result);
88
+ return result;
89
+ }
90
+
91
+ function buildScalarValue(meta: any, columnName: string, seedRow: any) {
92
+ const seedValue = seedRow?.[columnName];
93
+ const mysqlType = String(meta?.MYSQL_TYPE ?? '').toLowerCase();
94
+
95
+ if (mysqlType === 'year') return new Date().getFullYear();
96
+
97
+ const geometryTypes = [
98
+ 'geometry',
99
+ 'point',
100
+ 'linestring',
101
+ 'polygon',
102
+ 'multipoint',
103
+ 'multilinestring',
104
+ 'multipolygon',
105
+ 'geometrycollection',
106
+ ];
107
+ if (geometryTypes.some((type) => mysqlType.includes(type))) {
108
+ return "ST_GeomFromText('POINT(0 0)')";
109
+ }
110
+
111
+ if (mysqlType === 'json') return {};
112
+ if (mysqlType === 'enum' || mysqlType === 'set') {
113
+ return seedValue;
114
+ }
115
+
116
+ const isStringType = ['char', 'varchar', 'text', 'tinytext', 'mediumtext', 'longtext'].some((t) =>
117
+ mysqlType.includes(t)
118
+ );
119
+ if (isStringType) {
120
+ const value = `${columnName}_${Date.now()}`;
121
+ const maxLength = parseInt(meta?.MAX_LENGTH ?? '', 10);
122
+ if (Number.isFinite(maxLength) && maxLength > 0) {
123
+ return value.slice(0, maxLength);
124
+ }
125
+ return value;
126
+ }
127
+
128
+ const isDateType = ['date', 'time', 'datetime', 'timestamp', 'year'].some((t) => mysqlType.includes(t));
129
+ if (isDateType) return formatTimestamp();
130
+
131
+ const isNumericType = [
132
+ 'int',
133
+ 'decimal',
134
+ 'numeric',
135
+ 'float',
136
+ 'double',
137
+ 'real',
138
+ 'bit',
139
+ ].some((t) => mysqlType.includes(t));
140
+ if (isNumericType) {
141
+ if (typeof seedValue === 'number') return seedValue + 1;
142
+ return 1;
143
+ }
144
+
145
+ if (mysqlType.includes('bool')) return 1;
146
+
147
+ if (['blob', 'binary', 'varbinary'].some((t) => mysqlType.includes(t))) {
148
+ return Buffer.from('00', 'hex');
149
+ }
150
+
151
+ if (seedValue !== undefined) return seedValue;
152
+
153
+ return null;
154
+ }
155
+
21
156
  async function waitForRequests(timeout = 10000) {
22
157
  const start = Date.now();
23
158
  while (!checkAllRequestsComplete()) {
@@ -28,6 +163,302 @@ async function waitForRequests(timeout = 10000) {
28
163
  }
29
164
  }
30
165
 
166
+ async function fetchSeedRow(binding: any, label: string) {
167
+ const result = await executeAndRecord(label, () =>
168
+ binding.Get({
169
+ [C6.PAGINATION]: { [C6.LIMIT]: 1 },
170
+ } as any)
171
+ );
172
+ const data = unwrapResponse(result);
173
+ return data?.rest?.[0];
174
+ }
175
+
176
+ async function pickForeignKeyValue({
177
+ columnName,
178
+ referencedTable,
179
+ referencedColumn,
180
+ restBinding,
181
+ label,
182
+ }: {
183
+ columnName: string;
184
+ referencedTable: string;
185
+ referencedColumn: string;
186
+ restBinding: any;
187
+ label: string;
188
+ }) {
189
+ const referencedBinding = getBinding(referencedTable);
190
+ if (!referencedBinding) return undefined;
191
+
192
+ const [currentResult, referencedResult] = await Promise.all([
193
+ executeAndRecord(`${label}.fk.current`, () =>
194
+ restBinding.Get({
195
+ [C6.PAGINATION]: { [C6.LIMIT]: 25 },
196
+ } as any)
197
+ ),
198
+ executeAndRecord(`${label}.fk.referenced`, () =>
199
+ referencedBinding.Get({
200
+ [C6.PAGINATION]: { [C6.LIMIT]: 25 },
201
+ } as any)
202
+ ),
203
+ ]);
204
+
205
+ const currentData = unwrapResponse(currentResult);
206
+ const referencedData = unwrapResponse(referencedResult);
207
+
208
+ const currentRows = currentData?.rest ?? [];
209
+ const currentValues = new Set(
210
+ currentRows
211
+ .map((row) => row?.[columnName])
212
+ .filter((value) => value !== undefined && value !== null)
213
+ );
214
+
215
+ const candidate = (referencedData?.rest ?? [])
216
+ .map((row) => row?.[referencedColumn])
217
+ .find((value) => value !== undefined && value !== null && !currentValues.has(value));
218
+
219
+ if (candidate !== undefined) return candidate;
220
+
221
+ if (currentValues.size < currentRows.length) {
222
+ return referencedData?.rest?.[0]?.[referencedColumn];
223
+ }
224
+
225
+ return undefined;
226
+ }
227
+
228
+ function buildUpdatedValue(meta: any, columnName: string, currentValue: any) {
229
+ const mysqlType = String(meta?.MYSQL_TYPE ?? '').toLowerCase();
230
+ const geometryTypes = [
231
+ 'geometry',
232
+ 'point',
233
+ 'linestring',
234
+ 'polygon',
235
+ 'multipoint',
236
+ 'multilinestring',
237
+ 'multipolygon',
238
+ 'geometrycollection',
239
+ ];
240
+
241
+ if (mysqlType === 'year') return new Date().getFullYear();
242
+ if (geometryTypes.some((type) => mysqlType.includes(type))) {
243
+ return "ST_GeomFromText('POINT(1 1)')";
244
+ }
245
+ if (mysqlType === 'json') return { updated: true };
246
+
247
+ const isDateType = ['date', 'time', 'datetime', 'timestamp'].some((t) => mysqlType.includes(t));
248
+ if (isDateType) return formatTimestamp();
249
+
250
+ const isNumericType = [
251
+ 'int',
252
+ 'decimal',
253
+ 'numeric',
254
+ 'float',
255
+ 'double',
256
+ 'real',
257
+ 'bit',
258
+ ].some((t) => mysqlType.includes(t));
259
+ if (isNumericType) {
260
+ if (typeof currentValue === 'number') return currentValue + 1;
261
+ return 1;
262
+ }
263
+
264
+ if (mysqlType.includes('bool')) return currentValue ? 0 : 1;
265
+
266
+ const isStringType = ['char', 'varchar', 'text', 'tinytext', 'mediumtext', 'longtext'].some((t) =>
267
+ mysqlType.includes(t)
268
+ );
269
+ if (isStringType) {
270
+ const base = `${columnName}_updated_${Date.now()}`;
271
+ const maxLength = parseInt(meta?.MAX_LENGTH ?? '', 10);
272
+ if (Number.isFinite(maxLength) && maxLength > 0) {
273
+ return base.slice(0, maxLength);
274
+ }
275
+ return base;
276
+ }
277
+
278
+ if (currentValue instanceof Date) {
279
+ return formatTimestamp();
280
+ }
281
+
282
+ return currentValue ?? null;
283
+ }
284
+
285
+ function normalizeForComparison(meta: any, value: any) {
286
+ if (value === undefined || value === null) return value;
287
+ const mysqlType = String(meta?.MYSQL_TYPE ?? '').toLowerCase();
288
+
289
+ const isDateType = ['date', 'time', 'datetime', 'timestamp', 'year'].some((t) => mysqlType.includes(t));
290
+ if (isDateType) {
291
+ const dateValue = value instanceof Date ? value : new Date(value);
292
+ const time = dateValue.getTime();
293
+ if (!Number.isNaN(time)) return time;
294
+ }
295
+
296
+ const isDecimalType = ['decimal', 'numeric'].some((t) => mysqlType.includes(t));
297
+ if (isDecimalType) {
298
+ const parsed = Number(value);
299
+ if (!Number.isNaN(parsed)) return parsed;
300
+ }
301
+
302
+ const isNumericType = [
303
+ 'int',
304
+ 'float',
305
+ 'double',
306
+ 'real',
307
+ 'bit',
308
+ ].some((t) => mysqlType.includes(t));
309
+ if (isNumericType) {
310
+ const parsed = Number(value);
311
+ if (!Number.isNaN(parsed)) return parsed;
312
+ }
313
+
314
+ return value;
315
+ }
316
+
317
+ function rowMatches(row: any, fields: Array<{ columnName: string; value: any; meta: any }>) {
318
+ if (!row) return false;
319
+ return fields.every(({ columnName, value, meta }) => {
320
+ const actual = normalizeForComparison(meta ?? {}, row?.[columnName]);
321
+ const expected = normalizeForComparison(meta ?? {}, value);
322
+ return actual === expected;
323
+ });
324
+ }
325
+
326
+ async function buildInsertPayload(restModel: any, restBinding: any, label: string) {
327
+ const seedRow = await fetchSeedRow(restBinding, `${label}.seed`);
328
+ if (!seedRow) return null;
329
+
330
+ const payload: Record<string, any> = {};
331
+ const where: Record<string, any> = {};
332
+ const compareFields: Array<{ columnName: string; value: any; meta: any }> = [];
333
+ const references: Record<string, any[]> = restModel.TABLE_REFERENCES ?? {};
334
+ const validations: Record<string, any> = restModel.TYPE_VALIDATION ?? {};
335
+ const columnMap: Record<string, string> = restModel.COLUMNS ?? {};
336
+ const metaByColumnName: Record<string, any> = {};
337
+ let missingRequired = false;
338
+
339
+ for (const [fullColumn, meta] of Object.entries(validations) as [string, any][]) {
340
+ const columnName = columnMap[fullColumn] ?? fullColumn.split('.').pop();
341
+ if (!columnName) continue;
342
+ metaByColumnName[columnName] = meta;
343
+ metaByColumnName[fullColumn] = meta;
344
+
345
+ if (meta.AUTO_INCREMENT) continue;
346
+ if (meta.SKIP_COLUMN_IN_POST) continue;
347
+
348
+ let value;
349
+ const refList = references[columnName];
350
+ if (Array.isArray(refList) && refList.length > 0) {
351
+ const ref = refList[0];
352
+ const referencedTable = stripPrefix(ref.TABLE);
353
+ value = await pickForeignKeyValue({
354
+ columnName,
355
+ referencedTable,
356
+ referencedColumn: ref.COLUMN,
357
+ restBinding,
358
+ label,
359
+ });
360
+ if (value === undefined) {
361
+ missingRequired = true;
362
+ break;
363
+ }
364
+ } else {
365
+ value = buildScalarValue(meta, columnName, seedRow);
366
+ }
367
+
368
+ if (value === undefined) {
369
+ if (seedRow?.[columnName] !== undefined) {
370
+ value = seedRow[columnName];
371
+ }
372
+ }
373
+
374
+ if (value === undefined) {
375
+ missingRequired = true;
376
+ break;
377
+ }
378
+
379
+ payload[columnName] = value;
380
+
381
+ const mysqlType = String(meta?.MYSQL_TYPE ?? '').toLowerCase();
382
+ const geometryTypes = [
383
+ 'geometry',
384
+ 'point',
385
+ 'linestring',
386
+ 'polygon',
387
+ 'multipoint',
388
+ 'multilinestring',
389
+ 'multipolygon',
390
+ 'geometrycollection',
391
+ ];
392
+ const isDateType = ['date', 'time', 'datetime', 'timestamp', 'year'].some((t) => mysqlType.includes(t));
393
+ const shouldSkipWhere =
394
+ isDateType
395
+ || geometryTypes.some((type) => mysqlType.includes(type))
396
+ || mysqlType === 'json'
397
+ || Array.isArray(value)
398
+ || value instanceof Date
399
+ || (typeof Buffer !== 'undefined' && Buffer.isBuffer && Buffer.isBuffer(value));
400
+
401
+ if (!shouldSkipWhere && fullColumn && value !== undefined && value !== null) {
402
+ where[fullColumn] = value;
403
+ compareFields.push({ columnName, value, meta });
404
+ }
405
+ }
406
+
407
+ if (missingRequired) return null;
408
+
409
+ if (!Object.keys(payload).length) return null;
410
+
411
+ if (!Object.keys(where).length) return null;
412
+
413
+ return { payload, where, metaByColumnName, compareFields };
414
+ }
415
+
416
+ function buildJoinRequest(shortName: string, restModel: any) {
417
+ const baseTable = restModel.TABLE_NAME ?? shortName;
418
+ const references: Record<string, any[]> = restModel.TABLE_REFERENCES ?? {};
419
+ const referencedBy: Record<string, any[]> = restModel.TABLE_REFERENCED_BY ?? {};
420
+
421
+ const referenceEntries = Object.entries(references) as [string, any[]][];
422
+ if (referenceEntries.length > 0) {
423
+ const [localColumn, refs] = referenceEntries[0];
424
+ const ref = refs[0];
425
+ const joinTable = ref.TABLE;
426
+ const joinAlias = `${stripPrefix(ref.TABLE)}_ref`;
427
+ return {
428
+ [C6.SELECT]: ['*'],
429
+ [C6.JOIN]: {
430
+ [C6.INNER]: {
431
+ [`${joinTable} ${joinAlias}`]: {
432
+ [`${joinAlias}.${ref.COLUMN}`]: [C6.EQUAL, `${baseTable}.${localColumn}`],
433
+ },
434
+ },
435
+ },
436
+ [C6.PAGINATION]: { [C6.LIMIT]: 1 },
437
+ };
438
+ }
439
+
440
+ const referencedByEntries = Object.entries(referencedBy) as [string, any[]][];
441
+ if (referencedByEntries.length > 0) {
442
+ const [localColumn, refs] = referencedByEntries[0];
443
+ const ref = refs[0];
444
+ const joinTable = ref.TABLE;
445
+ const joinAlias = `${stripPrefix(ref.TABLE)}_ref`;
446
+ return {
447
+ [C6.SELECT]: ['*'],
448
+ [C6.JOIN]: {
449
+ [C6.INNER]: {
450
+ [`${joinTable} ${joinAlias}`]: {
451
+ [`${joinAlias}.${ref.COLUMN}`]: [C6.EQUAL, `${baseTable}.${localColumn}`],
452
+ },
453
+ },
454
+ },
455
+ [C6.PAGINATION]: { [C6.LIMIT]: 1 },
456
+ };
457
+ }
458
+
459
+ return null;
460
+ }
461
+
31
462
  describe('sakila-db generated C6 bindings', () => {
32
463
  let pool;
33
464
 
@@ -43,48 +474,163 @@ describe('sakila-db generated C6 bindings', () => {
43
474
 
44
475
  afterAll(async () => {
45
476
  await pool.end();
477
+ await finalizeSqlAllowList();
46
478
  });
47
479
 
48
- for (const [shortName] of Object.entries(C6.TABLES)) {
49
- const restBinding = C6.ORM[toPascalCase(shortName)];
480
+ for (const [shortName, restModel] of Object.entries(C6.TABLES as Record<string, any>)) {
481
+ const restBinding = getBinding(shortName);
482
+ const tableModel = restModel as any;
50
483
  if (!restBinding) continue;
51
484
 
52
485
  it(`[${shortName}] GET`, async () => {
53
- const result = await restBinding.Get({
54
- SELECT: ['*'],
55
- [C6.PAGINATION]: { [C6.LIMIT]: 1 },
56
- } as any);
57
- const data = result?.data ?? result;
486
+ const result = await executeAndRecord(`${shortName}.get`, () =>
487
+ restBinding.Get({
488
+ [C6.SELECT]: ['*'],
489
+ [C6.PAGINATION]: { [C6.LIMIT]: 1 },
490
+ } as any)
491
+ );
492
+ const data = unwrapResponse(result);
493
+ expect(Array.isArray(data?.rest)).toBe(true);
494
+ await waitForRequests();
495
+ });
496
+
497
+ it(`[${shortName}] JOIN`, async () => {
498
+ const joinRequest = buildJoinRequest(shortName, tableModel);
499
+ if (!joinRequest) return;
500
+
501
+ const result = await executeAndRecord(`${shortName}.join`, () =>
502
+ restBinding.Get(joinRequest as any)
503
+ );
504
+ const data = unwrapResponse(result);
58
505
  expect(Array.isArray(data?.rest)).toBe(true);
59
506
  await waitForRequests();
60
507
  });
61
- }
62
508
 
63
- it('[actor] PUT fully qualified keys', async () => {
64
- const Actor = C6.ORM.Actor;
65
- const testId = 1;
509
+ it(`[${shortName}] POST/PUT/DELETE`, async () => {
510
+ const primaryKeys = tableModel.PRIMARY_SHORT ?? [];
511
+ if (primaryKeys.length !== 1) return;
66
512
 
67
- let result = await Actor.Get({ [Actor.ACTOR_ID]: testId } as any);
68
- let data = result?.data ?? result;
69
- const originalLastName = data?.rest?.[0]?.last_name;
513
+ const payloadSpec = await buildInsertPayload(tableModel, restBinding, shortName);
514
+ if (!payloadSpec) return;
70
515
 
71
- await Actor.Put({
72
- [Actor.ACTOR_ID]: testId,
73
- [Actor.LAST_NAME]: 'Updated',
74
- } as any);
516
+ await executeAndRecord(`${shortName}.post`, () =>
517
+ restBinding.Post(payloadSpec.payload as any)
518
+ );
75
519
 
76
- result = await Actor.Get({
77
- [Actor.ACTOR_ID]: testId,
78
- cacheResults: false,
79
- } as any);
80
- data = result?.data ?? result;
81
- expect(data?.rest?.[0]?.last_name).toBe('Updated');
520
+ const primaryFull = tableModel.PRIMARY?.[0];
521
+ const primaryKey = primaryKeys[0];
522
+ const primaryMeta = payloadSpec.metaByColumnName?.[primaryKey]
523
+ ?? payloadSpec.metaByColumnName?.[primaryFull ?? ''];
82
524
 
83
- await Actor.Put({
84
- [Actor.ACTOR_ID]: testId,
85
- [Actor.LAST_NAME]: originalLastName,
86
- } as any);
87
- await waitForRequests();
88
- });
89
- });
525
+ let insertedRow: any;
90
526
 
527
+ if (primaryFull && primaryMeta?.AUTO_INCREMENT) {
528
+ const latestResult = await executeAndRecord(`${shortName}.post.latest`, () =>
529
+ restBinding.Get({
530
+ [C6.PAGINATION]: {
531
+ [C6.LIMIT]: 1,
532
+ [C6.ORDER]: { [primaryFull]: 'DESC' },
533
+ },
534
+ cacheResults: false,
535
+ } as any)
536
+ );
537
+ const latestData = unwrapResponse(latestResult);
538
+ insertedRow = latestData?.rest?.[0];
539
+ } else {
540
+ const lookupResult = await executeAndRecord(`${shortName}.post.lookup`, () =>
541
+ restBinding.Get({
542
+ [C6.WHERE]: payloadSpec.where,
543
+ [C6.PAGINATION]: { [C6.LIMIT]: 1 },
544
+ cacheResults: false,
545
+ } as any)
546
+ );
547
+ const lookupData = unwrapResponse(lookupResult);
548
+ insertedRow = (lookupData?.rest ?? []).find((row: any) =>
549
+ rowMatches(row, payloadSpec.compareFields ?? [])
550
+ );
551
+
552
+ if (!insertedRow && primaryFull) {
553
+ const fallbackResult = await executeAndRecord(`${shortName}.post.fallback`, () =>
554
+ restBinding.Get({
555
+ [C6.PAGINATION]: {
556
+ [C6.LIMIT]: 1,
557
+ [C6.ORDER]: { [primaryFull]: 'DESC' },
558
+ },
559
+ cacheResults: false,
560
+ } as any)
561
+ );
562
+ const fallbackData = unwrapResponse(fallbackResult);
563
+ insertedRow = fallbackData?.rest?.[0];
564
+ }
565
+ }
566
+
567
+ expect(insertedRow).toBeDefined();
568
+
569
+ if (!insertedRow) return;
570
+
571
+ const primaryValue = insertedRow[primaryKey];
572
+ if (primaryValue === undefined || primaryValue === null) return;
573
+
574
+ const foreignKeys = new Set(Object.keys(tableModel.TABLE_REFERENCES ?? {}));
575
+ const updateColumn = Object.keys(payloadSpec.payload)
576
+ .find((key) => {
577
+ if (key === primaryKey || foreignKeys.has(key)) return false;
578
+ const meta = payloadSpec.metaByColumnName?.[key];
579
+ const mysqlType = String(meta?.MYSQL_TYPE ?? '').toLowerCase();
580
+ const geometryTypes = [
581
+ 'geometry',
582
+ 'point',
583
+ 'linestring',
584
+ 'polygon',
585
+ 'multipoint',
586
+ 'multilinestring',
587
+ 'multipolygon',
588
+ 'geometrycollection',
589
+ ];
590
+ return !geometryTypes.some((type) => mysqlType.includes(type));
591
+ })
592
+ ?? Object.keys(payloadSpec.payload).find((key) => key !== primaryKey);
593
+
594
+ if (!updateColumn) return;
595
+
596
+ const currentValue = insertedRow[updateColumn];
597
+ const updateMeta = payloadSpec.metaByColumnName?.[updateColumn];
598
+ const updatedValue = buildUpdatedValue(updateMeta ?? {}, updateColumn, currentValue);
599
+
600
+ await executeAndRecord(`${shortName}.put`, () =>
601
+ restBinding.Put({
602
+ [primaryKey]: primaryValue,
603
+ [updateColumn]: updatedValue,
604
+ } as any)
605
+ );
606
+
607
+ const updatedResult = await executeAndRecord(`${shortName}.put.lookup`, () =>
608
+ restBinding.Get({
609
+ [primaryKey]: primaryValue,
610
+ cacheResults: false,
611
+ } as any)
612
+ );
613
+ const updatedData = unwrapResponse(updatedResult);
614
+ const normalizedActual = normalizeForComparison(updateMeta ?? {}, updatedData?.rest?.[0]?.[updateColumn]);
615
+ const normalizedExpected = normalizeForComparison(updateMeta ?? {}, updatedValue);
616
+ expect(normalizedActual).toBe(normalizedExpected);
617
+
618
+ await executeAndRecord(`${shortName}.delete`, () =>
619
+ restBinding.Delete({
620
+ [primaryKey]: primaryValue,
621
+ } as any)
622
+ );
623
+
624
+ const deletedResult = await executeAndRecord(`${shortName}.delete.lookup`, () =>
625
+ restBinding.Get({
626
+ [primaryKey]: primaryValue,
627
+ cacheResults: false,
628
+ } as any)
629
+ );
630
+ const deletedData = unwrapResponse(deletedResult);
631
+ expect(Array.isArray(deletedData?.rest)).toBe(true);
632
+ expect(deletedData?.rest?.length ?? 0).toBe(0);
633
+ await waitForRequests();
634
+ });
635
+ }
636
+ });
@@ -59,15 +59,15 @@ var MySQLDump = /** @class */ (function () {
59
59
  ];
60
60
  cnf.push("");
61
61
  if ('' === cnfFile) {
62
- cnfFile = path.join(process.cwd(), '/mysql.cnf');
62
+ cnfFile = path.join(this.OUTPUT_DIR, 'C6.mysql.cnf');
63
63
  }
64
64
  try {
65
65
  fs.writeFileSync(cnfFile, cnf.join('\n'));
66
66
  fs.chmodSync(cnfFile, 488);
67
- console.log("Successfully created mysql.cnf file in (".concat(cnfFile, ")"));
67
+ console.log("Successfully created C6.mysql.cnf file in (".concat(cnfFile, ")"));
68
68
  }
69
69
  catch (error) {
70
- console.error("Failed to store file contents of mysql.cnf in (".concat(process.cwd(), ")"), error);
70
+ console.error("Failed to store file contents of C6.mysql.cnf in (".concat(process.cwd(), ")"), error);
71
71
  process.exit(1);
72
72
  }
73
73
  return (this.mysqlcnf = cnfFile);
@@ -80,7 +80,7 @@ var MySQLDump = /** @class */ (function () {
80
80
  if (otherOption === void 0) { otherOption = ''; }
81
81
  if (specificTable === void 0) { specificTable = ''; }
82
82
  if (outputFile === '') {
83
- outputFile = path.join(process.cwd(), 'mysqldump.sql');
83
+ outputFile = path.join(this.OUTPUT_DIR, 'C6.mysqldump.sql');
84
84
  }
85
85
  if (!data && !schemas) {
86
86
  console.warn("MysqlDump is running with --no-create-info and --no-data. Why?");
@@ -404,7 +404,7 @@ var parseSQLToTypeScript = function (sql) {
404
404
  var sql = fs.readFileSync(dumpFileLocation, 'utf-8');
405
405
  var tableData = parseSQLToTypeScript(sql);
406
406
  // write to file
407
- fs.writeFileSync(path.join(process.cwd(), 'C6MySqlDump.json'), JSON.stringify(tableData));
407
+ fs.writeFileSync(path.join(MySQLDump.OUTPUT_DIR, 'C6.mysqldump.json'), JSON.stringify(tableData));
408
408
  // import this file src/assets/handlebars/C6.tsx.handlebars for a mustache template
409
409
  var c6Template = fs.readFileSync(path.resolve(__dirname, 'assets/handlebars/C6.ts.handlebars'), 'utf-8');
410
410
  var c6TestTemplate = fs.readFileSync(path.resolve(__dirname, 'assets/handlebars/C6.test.ts.handlebars'), 'utf-8');