@carbonorm/carbonnode 4.0.0 → 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 (158) hide show
  1. package/README.md +246 -507
  2. package/dist/api/executors/SqlExecutor.d.ts +6 -0
  3. package/dist/api/handlers/ExpressHandler.d.ts +2 -1
  4. package/dist/api/orm/builders/ConditionBuilder.d.ts +2 -0
  5. package/dist/api/types/ormInterfaces.d.ts +12 -0
  6. package/dist/api/utils/sqlAllowList.d.ts +2 -0
  7. package/dist/index.cjs.js +279 -20
  8. package/dist/index.cjs.js.map +1 -1
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.esm.js +278 -21
  11. package/dist/index.esm.js.map +1 -1
  12. package/package.json +1 -1
  13. package/scripts/assets/handlebars/C6.test.ts.handlebars +578 -32
  14. package/scripts/generateRestBindings.cjs +5 -5
  15. package/scripts/generateRestBindings.ts +5 -5
  16. package/src/__tests__/fixtures/createTestServer.ts +11 -3
  17. package/src/__tests__/fixtures/sqlResponses/actor.get.json +13 -0
  18. package/src/__tests__/fixtures/sqlResponses/sqlAllowList.blocked.json +3 -0
  19. package/src/__tests__/fixtures/sqlResponses/sqlAllowList.json +3 -0
  20. package/src/__tests__/sakila-db/C6.js +1 -1
  21. package/src/__tests__/sakila-db/C6.mysql.cnf +6 -0
  22. package/src/__tests__/sakila-db/C6.mysqldump.json +1 -0
  23. package/src/__tests__/sakila-db/C6.mysqldump.sql +720 -0
  24. package/src/__tests__/sakila-db/C6.sqlAllowList.json +94 -0
  25. package/src/__tests__/sakila-db/C6.test.ts +578 -32
  26. package/src/__tests__/sakila-db/C6.ts +1 -1
  27. package/src/__tests__/sakila-db/sqlResponses/C6.actor.delete.json +10 -0
  28. package/src/__tests__/sakila-db/sqlResponses/C6.actor.delete.lookup.json +9 -0
  29. package/src/__tests__/sakila-db/sqlResponses/C6.actor.get.json +14 -0
  30. package/src/__tests__/sakila-db/sqlResponses/C6.actor.join.json +15 -0
  31. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.json +12 -0
  32. package/src/__tests__/sakila-db/sqlResponses/C6.actor.post.latest.json +14 -0
  33. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.json +11 -0
  34. package/src/__tests__/sakila-db/sqlResponses/C6.actor.put.lookup.json +16 -0
  35. package/src/__tests__/sakila-db/sqlResponses/C6.actor.seed.json +14 -0
  36. package/src/__tests__/sakila-db/sqlResponses/C6.address.delete.json +10 -0
  37. package/src/__tests__/sakila-db/sqlResponses/C6.address.delete.lookup.json +9 -0
  38. package/src/__tests__/sakila-db/sqlResponses/C6.address.fk.current.json +358 -0
  39. package/src/__tests__/sakila-db/sqlResponses/C6.address.fk.referenced.json +158 -0
  40. package/src/__tests__/sakila-db/sqlResponses/C6.address.get.json +22 -0
  41. package/src/__tests__/sakila-db/sqlResponses/C6.address.join.json +24 -0
  42. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.json +16 -0
  43. package/src/__tests__/sakila-db/sqlResponses/C6.address.post.latest.json +22 -0
  44. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.json +11 -0
  45. package/src/__tests__/sakila-db/sqlResponses/C6.address.put.lookup.json +24 -0
  46. package/src/__tests__/sakila-db/sqlResponses/C6.address.seed.json +22 -0
  47. package/src/__tests__/sakila-db/sqlResponses/C6.category.delete.json +10 -0
  48. package/src/__tests__/sakila-db/sqlResponses/C6.category.delete.lookup.json +9 -0
  49. package/src/__tests__/sakila-db/sqlResponses/C6.category.get.json +13 -0
  50. package/src/__tests__/sakila-db/sqlResponses/C6.category.join.json +14 -0
  51. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.json +11 -0
  52. package/src/__tests__/sakila-db/sqlResponses/C6.category.post.latest.json +13 -0
  53. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.json +11 -0
  54. package/src/__tests__/sakila-db/sqlResponses/C6.category.put.lookup.json +15 -0
  55. package/src/__tests__/sakila-db/sqlResponses/C6.category.seed.json +13 -0
  56. package/src/__tests__/sakila-db/sqlResponses/C6.city.delete.json +10 -0
  57. package/src/__tests__/sakila-db/sqlResponses/C6.city.delete.lookup.json +9 -0
  58. package/src/__tests__/sakila-db/sqlResponses/C6.city.fk.current.json +158 -0
  59. package/src/__tests__/sakila-db/sqlResponses/C6.city.fk.referenced.json +133 -0
  60. package/src/__tests__/sakila-db/sqlResponses/C6.city.get.json +14 -0
  61. package/src/__tests__/sakila-db/sqlResponses/C6.city.join.json +15 -0
  62. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.json +12 -0
  63. package/src/__tests__/sakila-db/sqlResponses/C6.city.post.latest.json +14 -0
  64. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.json +11 -0
  65. package/src/__tests__/sakila-db/sqlResponses/C6.city.put.lookup.json +16 -0
  66. package/src/__tests__/sakila-db/sqlResponses/C6.city.seed.json +14 -0
  67. package/src/__tests__/sakila-db/sqlResponses/C6.country.delete.json +10 -0
  68. package/src/__tests__/sakila-db/sqlResponses/C6.country.delete.lookup.json +9 -0
  69. package/src/__tests__/sakila-db/sqlResponses/C6.country.get.json +13 -0
  70. package/src/__tests__/sakila-db/sqlResponses/C6.country.join.json +15 -0
  71. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.json +11 -0
  72. package/src/__tests__/sakila-db/sqlResponses/C6.country.post.latest.json +13 -0
  73. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.json +11 -0
  74. package/src/__tests__/sakila-db/sqlResponses/C6.country.put.lookup.json +15 -0
  75. package/src/__tests__/sakila-db/sqlResponses/C6.country.seed.json +13 -0
  76. package/src/__tests__/sakila-db/sqlResponses/C6.customer.delete.json +10 -0
  77. package/src/__tests__/sakila-db/sqlResponses/C6.customer.delete.lookup.json +9 -0
  78. package/src/__tests__/sakila-db/sqlResponses/C6.customer.fk.current.json +283 -0
  79. package/src/__tests__/sakila-db/sqlResponses/C6.customer.fk.referenced.json +358 -0
  80. package/src/__tests__/sakila-db/sqlResponses/C6.customer.get.json +19 -0
  81. package/src/__tests__/sakila-db/sqlResponses/C6.customer.join.json +29 -0
  82. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.json +17 -0
  83. package/src/__tests__/sakila-db/sqlResponses/C6.customer.post.latest.json +19 -0
  84. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.json +11 -0
  85. package/src/__tests__/sakila-db/sqlResponses/C6.customer.put.lookup.json +21 -0
  86. package/src/__tests__/sakila-db/sqlResponses/C6.customer.seed.json +19 -0
  87. package/src/__tests__/sakila-db/sqlResponses/C6.film.delete.json +10 -0
  88. package/src/__tests__/sakila-db/sqlResponses/C6.film.delete.lookup.json +9 -0
  89. package/src/__tests__/sakila-db/sqlResponses/C6.film.fk.current.json +383 -0
  90. package/src/__tests__/sakila-db/sqlResponses/C6.film.fk.referenced.json +38 -0
  91. package/src/__tests__/sakila-db/sqlResponses/C6.film.get.json +23 -0
  92. package/src/__tests__/sakila-db/sqlResponses/C6.film.join.json +24 -0
  93. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.json +20 -0
  94. package/src/__tests__/sakila-db/sqlResponses/C6.film.post.latest.json +23 -0
  95. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.json +11 -0
  96. package/src/__tests__/sakila-db/sqlResponses/C6.film.put.lookup.json +25 -0
  97. package/src/__tests__/sakila-db/sqlResponses/C6.film.seed.json +23 -0
  98. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.delete.json +10 -0
  99. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.delete.lookup.json +9 -0
  100. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.fk.current.json +158 -0
  101. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.fk.referenced.json +20 -0
  102. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.get.json +14 -0
  103. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.join.json +25 -0
  104. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.json +12 -0
  105. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.post.latest.json +14 -0
  106. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.json +11 -0
  107. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.put.lookup.json +16 -0
  108. package/src/__tests__/sakila-db/sqlResponses/C6.inventory.seed.json +14 -0
  109. package/src/__tests__/sakila-db/sqlResponses/C6.language.delete.json +10 -0
  110. package/src/__tests__/sakila-db/sqlResponses/C6.language.delete.lookup.json +9 -0
  111. package/src/__tests__/sakila-db/sqlResponses/C6.language.get.json +13 -0
  112. package/src/__tests__/sakila-db/sqlResponses/C6.language.join.json +24 -0
  113. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.json +11 -0
  114. package/src/__tests__/sakila-db/sqlResponses/C6.language.post.latest.json +13 -0
  115. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.json +11 -0
  116. package/src/__tests__/sakila-db/sqlResponses/C6.language.put.lookup.json +15 -0
  117. package/src/__tests__/sakila-db/sqlResponses/C6.language.seed.json +13 -0
  118. package/src/__tests__/sakila-db/sqlResponses/C6.payment.delete.json +10 -0
  119. package/src/__tests__/sakila-db/sqlResponses/C6.payment.delete.lookup.json +9 -0
  120. package/src/__tests__/sakila-db/sqlResponses/C6.payment.fk.current.json +233 -0
  121. package/src/__tests__/sakila-db/sqlResponses/C6.payment.fk.referenced.json +233 -0
  122. package/src/__tests__/sakila-db/sqlResponses/C6.payment.get.json +17 -0
  123. package/src/__tests__/sakila-db/sqlResponses/C6.payment.join.json +24 -0
  124. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.json +15 -0
  125. package/src/__tests__/sakila-db/sqlResponses/C6.payment.post.latest.json +17 -0
  126. package/src/__tests__/sakila-db/sqlResponses/C6.payment.put.json +11 -0
  127. package/src/__tests__/sakila-db/sqlResponses/C6.payment.put.lookup.json +19 -0
  128. package/src/__tests__/sakila-db/sqlResponses/C6.payment.seed.json +17 -0
  129. package/src/__tests__/sakila-db/sqlResponses/C6.rental.delete.json +10 -0
  130. package/src/__tests__/sakila-db/sqlResponses/C6.rental.delete.lookup.json +9 -0
  131. package/src/__tests__/sakila-db/sqlResponses/C6.rental.fk.current.json +233 -0
  132. package/src/__tests__/sakila-db/sqlResponses/C6.rental.fk.referenced.json +34 -0
  133. package/src/__tests__/sakila-db/sqlResponses/C6.rental.get.json +17 -0
  134. package/src/__tests__/sakila-db/sqlResponses/C6.rental.join.json +24 -0
  135. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.json +15 -0
  136. package/src/__tests__/sakila-db/sqlResponses/C6.rental.post.latest.json +17 -0
  137. package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.json +11 -0
  138. package/src/__tests__/sakila-db/sqlResponses/C6.rental.put.lookup.json +19 -0
  139. package/src/__tests__/sakila-db/sqlResponses/C6.rental.seed.json +17 -0
  140. package/src/__tests__/sakila-db/sqlResponses/C6.staff.fk.current.json +34 -0
  141. package/src/__tests__/sakila-db/sqlResponses/C6.staff.fk.referenced.json +20 -0
  142. package/src/__tests__/sakila-db/sqlResponses/C6.staff.get.json +21 -0
  143. package/src/__tests__/sakila-db/sqlResponses/C6.staff.join.json +31 -0
  144. package/src/__tests__/sakila-db/sqlResponses/C6.staff.seed.json +21 -0
  145. package/src/__tests__/sakila-db/sqlResponses/C6.store.fk.current.json +20 -0
  146. package/src/__tests__/sakila-db/sqlResponses/C6.store.fk.referenced.json +34 -0
  147. package/src/__tests__/sakila-db/sqlResponses/C6.store.get.json +14 -0
  148. package/src/__tests__/sakila-db/sqlResponses/C6.store.join.json +24 -0
  149. package/src/__tests__/sakila-db/sqlResponses/C6.store.seed.json +14 -0
  150. package/src/__tests__/sakila.generated.test.ts +31 -0
  151. package/src/__tests__/sqlAllowList.test.ts +135 -0
  152. package/src/__tests__/sqlBuilders.test.ts +17 -0
  153. package/src/api/executors/SqlExecutor.ts +156 -0
  154. package/src/api/handlers/ExpressHandler.ts +10 -1
  155. package/src/api/orm/builders/ConditionBuilder.ts +27 -7
  156. package/src/api/types/ormInterfaces.ts +15 -0
  157. package/src/api/utils/sqlAllowList.ts +54 -0
  158. package/src/index.ts +1 -0
@@ -7,7 +7,15 @@ import {iC6Object, iRestMethods} from "../types/ormInterfaces";
7
7
 
8
8
  // TODO - WE MUST make this a generic - optional, but helpful
9
9
  // note sure how it would help anyone actually...
10
- export function ExpressHandler({C6, mysqlPool}: { C6: iC6Object, mysqlPool: Pool }) {
10
+ export function ExpressHandler({
11
+ C6,
12
+ mysqlPool,
13
+ sqlAllowListPath,
14
+ }: {
15
+ C6: iC6Object;
16
+ mysqlPool: Pool;
17
+ sqlAllowListPath?: string;
18
+ }) {
11
19
 
12
20
  return async (req: Request, res: Response, next: NextFunction) => {
13
21
  try {
@@ -92,6 +100,7 @@ export function ExpressHandler({C6, mysqlPool}: { C6: iC6Object, mysqlPool: Pool
92
100
  const response = await restRequest({
93
101
  C6,
94
102
  mysqlPool,
103
+ sqlAllowListPath,
95
104
  requestMethod: method,
96
105
  restModel: C6.TABLES[table]
97
106
  })(payload);
@@ -378,19 +378,19 @@ export abstract class ConditionBuilder<
378
378
  throw new Error('Unsupported operand type in SQL expression.');
379
379
  }
380
380
 
381
- private isPlainArrayLiteral(value: any): boolean {
381
+ private isPlainArrayLiteral(value: any, allowColumnRefs = false): boolean {
382
382
  if (!Array.isArray(value)) return false;
383
383
  return value.every(item => {
384
384
  if (item === null) return true;
385
385
  const type = typeof item;
386
386
  if (type === 'string' || type === 'number' || type === 'boolean') return true;
387
- if (Array.isArray(item)) return this.isPlainArrayLiteral(item);
388
- if (item && typeof item === 'object') return this.isPlainObjectLiteral(item);
387
+ if (Array.isArray(item)) return this.isPlainArrayLiteral(item, allowColumnRefs);
388
+ if (item && typeof item === 'object') return this.isPlainObjectLiteral(item, allowColumnRefs);
389
389
  return false;
390
390
  });
391
391
  }
392
392
 
393
- private isPlainObjectLiteral(value: any): boolean {
393
+ private isPlainObjectLiteral(value: any, allowColumnRefs = false): boolean {
394
394
  if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
395
395
  if (value instanceof Date) return false;
396
396
  if (typeof Buffer !== 'undefined' && Buffer.isBuffer && Buffer.isBuffer(value)) return false;
@@ -405,13 +405,30 @@ export abstract class ConditionBuilder<
405
405
  return false;
406
406
  }
407
407
 
408
- if (entries.some(([key]) => typeof key === 'string' && (this.isColumnRef(key) || key.includes('.')))) {
409
- return false;
408
+ if (!allowColumnRefs) {
409
+ if (entries.some(([key]) => typeof key === 'string' && (this.isColumnRef(key) || key.includes('.')))) {
410
+ return false;
411
+ }
410
412
  }
411
413
 
412
414
  return true;
413
415
  }
414
416
 
417
+ private resolveColumnDefinition(column?: string): any | undefined {
418
+ if (!column || typeof column !== 'string' || !column.includes('.')) return undefined;
419
+ const [prefix, colName] = column.split('.', 2);
420
+ const tableName = this.aliasMap[prefix] ?? prefix;
421
+ const table = this.config.C6?.TABLES?.[tableName];
422
+ if (!table?.TYPE_VALIDATION) return undefined;
423
+ return table.TYPE_VALIDATION[colName] ?? table.TYPE_VALIDATION[`${tableName}.${colName}`];
424
+ }
425
+
426
+ private isJsonColumn(column?: string): boolean {
427
+ const columnDef = this.resolveColumnDefinition(column);
428
+ const mysqlType = columnDef?.MYSQL_TYPE;
429
+ return typeof mysqlType === 'string' && mysqlType.toLowerCase().includes('json');
430
+ }
431
+
415
432
  protected serializeUpdateValue(
416
433
  value: any,
417
434
  params: any[] | Record<string, any>,
@@ -419,7 +436,10 @@ export abstract class ConditionBuilder<
419
436
  ): string {
420
437
  const normalized = value instanceof Map ? Object.fromEntries(value) : value;
421
438
 
422
- if (this.isPlainArrayLiteral(normalized) || this.isPlainObjectLiteral(normalized)) {
439
+ const allowColumnRefs = this.isJsonColumn(contextColumn);
440
+
441
+ if (this.isPlainArrayLiteral(normalized, allowColumnRefs)
442
+ || this.isPlainObjectLiteral(normalized, allowColumnRefs)) {
423
443
  return this.addParam(params, contextColumn ?? '', JSON.stringify(normalized));
424
444
  }
425
445
 
@@ -162,6 +162,19 @@ export type DetermineResponseDataType<
162
162
  ? iDeleteC6RestResponse<RestTableInterface>
163
163
  : never);
164
164
 
165
+
166
+ export type iRestWebsocketPayload = {
167
+ REST: {
168
+ TABLE_NAME: string;
169
+ TABLE_PREFIX: string;
170
+ METHOD: iRestMethods;
171
+ REQUEST: Record<string, any>;
172
+ REQUEST_PRIMARY_KEY: Record<string, any> | null;
173
+ };
174
+ };
175
+
176
+ export type tWebsocketBroadcast = (payload: iRestWebsocketPayload) => void | Promise<void>;
177
+
165
178
  export interface iRest<
166
179
  RestShortTableName extends string = any,
167
180
  RestTableInterface extends Record<string, any> = any,
@@ -177,7 +190,9 @@ export interface iRest<
177
190
  requestMethod: iRestMethods;
178
191
  clearCache?: () => void;
179
192
  skipPrimaryCheck?: boolean;
193
+ websocketBroadcast?: tWebsocketBroadcast;
180
194
  verbose?: boolean;
195
+ sqlAllowListPath?: string;
181
196
  }
182
197
 
183
198
  export interface iConstraint {
@@ -0,0 +1,54 @@
1
+ import isNode from "../../variables/isNode";
2
+
3
+ const allowListCache = new Map<string, Set<string>>();
4
+
5
+ export const normalizeSql = (sql: string): string =>
6
+ sql.replace(/\s+/g, " ").trim();
7
+
8
+ const parseAllowList = (raw: string, sourcePath: string): string[] => {
9
+ let parsed: unknown;
10
+ try {
11
+ parsed = JSON.parse(raw);
12
+ } catch (error) {
13
+ throw new Error(`SQL allowlist at ${sourcePath} is not valid JSON.`);
14
+ }
15
+
16
+ if (!Array.isArray(parsed)) {
17
+ throw new Error(`SQL allowlist at ${sourcePath} must be a JSON array of strings.`);
18
+ }
19
+
20
+ const sqlEntries = parsed
21
+ .filter((entry): entry is string => typeof entry === "string")
22
+ .map(normalizeSql)
23
+ .filter((entry) => entry.length > 0);
24
+
25
+ if (sqlEntries.length !== parsed.length) {
26
+ throw new Error(`SQL allowlist at ${sourcePath} must contain only string entries.`);
27
+ }
28
+
29
+ return sqlEntries;
30
+ };
31
+
32
+ export const loadSqlAllowList = async (allowListPath: string): Promise<Set<string>> => {
33
+ if (allowListCache.has(allowListPath)) {
34
+ return allowListCache.get(allowListPath)!;
35
+ }
36
+
37
+ if (!isNode()) {
38
+ throw new Error("SQL allowlist validation requires a Node runtime.");
39
+ }
40
+
41
+ const {readFile} = await import("node:fs/promises");
42
+
43
+ let raw: string;
44
+ try {
45
+ raw = await readFile(allowListPath, "utf-8");
46
+ } catch (error) {
47
+ throw new Error(`SQL allowlist file not found at ${allowListPath}.`);
48
+ }
49
+
50
+ const sqlEntries = parseAllowList(raw, allowListPath);
51
+ const allowList = new Set(sqlEntries);
52
+ allowListCache.set(allowListPath, allowList);
53
+ return allowList;
54
+ };
package/src/index.ts CHANGED
@@ -37,6 +37,7 @@ export * from "./api/utils/determineRuntimeJsType";
37
37
  export * from "./api/utils/logger";
38
38
  export * from "./api/utils/normalizeSingularRequest";
39
39
  export * from "./api/utils/sortAndSerializeQueryObject";
40
+ export * from "./api/utils/sqlAllowList";
40
41
  export * from "./api/utils/testHelpers";
41
42
  export * from "./api/utils/toastNotifier";
42
43
  export * from "./variables/getEnvVar";