@carbonorm/carbonnode 3.7.6 → 3.7.8

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 +2 -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 +182 -32
  7. package/dist/index.cjs.js.map +1 -1
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.esm.js +183 -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 +105 -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 +144 -0
  38. package/src/index.ts +1 -0
@@ -89,7 +89,10 @@ var MySQLDump = /** @class */ (function () {
89
89
  var hexBlobOption = data ? '--hex-blob ' : '--no-data ';
90
90
  var createInfoOption = schemas ? '' : ' --no-create-info ';
91
91
  var cmd = "".concat(mysqldump, " --defaults-extra-file=\"").concat(defaultsExtraFile, "\" ").concat(otherOption, " --set-gtid-purged=\"OFF\" --skip-add-locks --lock-tables=false --single-transaction --quick ").concat(createInfoOption).concat(hexBlobOption).concat(this.DB_NAME, " ").concat(specificTable, " > '").concat(outputFile, "'");
92
- this.executeAndCheckStatus(cmd);
92
+ this.executeAndCheckStatus(cmd, false);
93
+ if (!fs.existsSync(outputFile)) {
94
+ console.warn("[generateRestBindings] mysqldump output not found at ".concat(outputFile, ". If running in CI/no-DB environment, ensure a prebuilt dump file exists at this path."));
95
+ }
93
96
  return (this.mysqldump = outputFile);
94
97
  };
95
98
  MySQLDump.executeAndCheckStatus = function (command, exitOnFailure, output) {
@@ -112,9 +115,9 @@ var MySQLDump = /** @class */ (function () {
112
115
  MySQLDump.DB_PASS = argMap['--pass'] || 'password';
113
116
  MySQLDump.DB_HOST = argMap['--host'] || '127.0.0.1';
114
117
  MySQLDump.DB_PORT = argMap['--port'] || '3306';
115
- MySQLDump.DB_NAME = argMap['--dbname'] || 'assessorly';
116
- MySQLDump.DB_PREFIX = argMap['--prefix'] || 'carbon_';
117
- MySQLDump.RELATIVE_OUTPUT_DIR = argMap['--output'] || '/src/api/rest';
118
+ MySQLDump.DB_NAME = argMap['--dbname'] || 'sakila';
119
+ MySQLDump.DB_PREFIX = argMap['--prefix'] || '';
120
+ MySQLDump.RELATIVE_OUTPUT_DIR = argMap['--output'] || '/src';
118
121
  MySQLDump.OUTPUT_DIR = path.join(process.cwd(), MySQLDump.RELATIVE_OUTPUT_DIR);
119
122
  return MySQLDump;
120
123
  }());
@@ -405,6 +408,14 @@ fs.writeFileSync(path.join(process.cwd(), 'C6MySqlDump.json'), JSON.stringify(ta
405
408
  // import this file src/assets/handlebars/C6.tsx.handlebars for a mustache template
406
409
  var c6Template = fs.readFileSync(path.resolve(__dirname, 'assets/handlebars/C6.ts.handlebars'), 'utf-8');
407
410
  var c6TestTemplate = fs.readFileSync(path.resolve(__dirname, 'assets/handlebars/C6.test.ts.handlebars'), 'utf-8');
408
- fs.writeFileSync(path.join(MySQLDump.OUTPUT_DIR, 'C6.ts'), Handlebars.compile(c6Template)(tableData));
409
- fs.writeFileSync(path.join(MySQLDump.OUTPUT_DIR, 'C6.test.ts'), Handlebars.compile(c6TestTemplate)(tableData));
411
+ var outputDir = MySQLDump.OUTPUT_DIR;
412
+ fs.writeFileSync(path.join(outputDir, 'C6.ts'), Handlebars.compile(c6Template)(tableData));
413
+ fs.writeFileSync(path.join(outputDir, 'C6.test.ts'), Handlebars.compile(c6TestTemplate)(tableData));
414
+ // compile generated TypeScript for runtime tests
415
+ try {
416
+ execSync("npx tsc ".concat(path.join(outputDir, 'C6.ts'), " --target ES2020 --module ES2020 --moduleResolution node --esModuleInterop --skipLibCheck --outDir ").concat(outputDir));
417
+ }
418
+ catch (e) {
419
+ console.warn('TypeScript compilation for generated C6.ts reported errors:', e);
420
+ }
410
421
  console.log('Successfully created CarbonORM bindings!');
@@ -24,9 +24,9 @@ class MySQLDump {
24
24
  static DB_PASS = argMap['--pass'] || 'password';
25
25
  static DB_HOST = argMap['--host'] || '127.0.0.1';
26
26
  static DB_PORT = argMap['--port'] || '3306';
27
- static DB_NAME = argMap['--dbname'] || 'assessorly';
28
- static DB_PREFIX = argMap['--prefix'] || 'carbon_';
29
- static RELATIVE_OUTPUT_DIR = argMap['--output'] || '/src/api/rest';
27
+ static DB_NAME = argMap['--dbname'] || 'sakila';
28
+ static DB_PREFIX = argMap['--prefix'] || '';
29
+ static RELATIVE_OUTPUT_DIR = argMap['--output'] || '/src';
30
30
  static OUTPUT_DIR = path.join(process.cwd(), MySQLDump.RELATIVE_OUTPUT_DIR);
31
31
 
32
32
  static buildCNF(cnfFile: string = '') {
@@ -92,7 +92,11 @@ class MySQLDump {
92
92
 
93
93
  const cmd = `${mysqldump} --defaults-extra-file="${defaultsExtraFile}" ${otherOption} --set-gtid-purged="OFF" --skip-add-locks --lock-tables=false --single-transaction --quick ${createInfoOption}${hexBlobOption}${this.DB_NAME} ${specificTable} > '${outputFile}'`;
94
94
 
95
- this.executeAndCheckStatus(cmd);
95
+ this.executeAndCheckStatus(cmd, false);
96
+
97
+ if (!fs.existsSync(outputFile)) {
98
+ console.warn(`[generateRestBindings] mysqldump output not found at ${outputFile}. If running in CI/no-DB environment, ensure a prebuilt dump file exists at this path.`);
99
+ }
96
100
 
97
101
  return (this.mysqldump = outputFile);
98
102
 
@@ -290,7 +294,8 @@ const parseSQLToTypeScript = (sql: string) => {
290
294
  .split(/,(?=(?:[^']*'[^']*')*[^']*$)/) // split only top-level commas
291
295
  .map(s => s.trim().replace(/^'(.*)'$/, '$1'))
292
296
  : null;
293
- const type = fullType.replace(/\(.+?\)/, '').split(' ')[0].toLowerCase(); const lengthMatch = fullType.match(/\(([^)]+)\)/);
297
+ const type = fullType.replace(/\(.+?\)/, '').split(' ')[0].toLowerCase();
298
+ const lengthMatch = fullType.match(/\(([^)]+)\)/);
294
299
  const length = lengthMatch ? lengthMatch[1] : '';
295
300
 
296
301
  const sridMatch = line.match(/SRID\s+(\d+)/i);
@@ -343,7 +348,7 @@ const parseSQLToTypeScript = (sql: string) => {
343
348
 
344
349
  }
345
350
 
346
- let REACT_IMPORT: false|string = false, CARBON_REACT_INSTANCE : false|string = false;
351
+ let REACT_IMPORT: false | string = false, CARBON_REACT_INSTANCE: false | string = false;
347
352
 
348
353
  if (argMap['--react']) {
349
354
 
@@ -489,7 +494,16 @@ fs.writeFileSync(path.join(process.cwd(), 'C6MySqlDump.json'), JSON.stringify(ta
489
494
  const c6Template = fs.readFileSync(path.resolve(__dirname, 'assets/handlebars/C6.ts.handlebars'), 'utf-8');
490
495
  const c6TestTemplate = fs.readFileSync(path.resolve(__dirname, 'assets/handlebars/C6.test.ts.handlebars'), 'utf-8');
491
496
 
492
- fs.writeFileSync(path.join(MySQLDump.OUTPUT_DIR, 'C6.ts'), Handlebars.compile(c6Template)(tableData));
493
- fs.writeFileSync(path.join(MySQLDump.OUTPUT_DIR, 'C6.test.ts'), Handlebars.compile(c6TestTemplate)(tableData));
497
+ const outputDir = MySQLDump.OUTPUT_DIR;
498
+
499
+ fs.writeFileSync(path.join(outputDir, 'C6.ts'), Handlebars.compile(c6Template)(tableData));
500
+ fs.writeFileSync(path.join(outputDir, 'C6.test.ts'), Handlebars.compile(c6TestTemplate)(tableData));
501
+
502
+ // compile generated TypeScript for runtime tests
503
+ try {
504
+ execSync(`npx tsc ${path.join(outputDir, 'C6.ts')} --target ES2020 --module ES2020 --moduleResolution node --esModuleInterop --skipLibCheck --outDir ${outputDir}`);
505
+ } catch (e) {
506
+ console.warn('TypeScript compilation for generated C6.ts reported errors:', e);
507
+ }
494
508
 
495
509
  console.log('Successfully created CarbonORM bindings!')
@@ -0,0 +1,74 @@
1
+ import type { iRest, C6RestfulModel } from "../../api/types/ormInterfaces";
2
+
3
+ // Minimal C6 table descriptor for tests
4
+ function tableModel<T extends Record<string, any>>(name: string, columns: Record<string, keyof T>): C6RestfulModel<string, T, keyof T & string> {
5
+ const TYPE_VALIDATION: any = {};
6
+ const COLUMNS: any = {};
7
+ Object.entries(columns).forEach(([fq, short]) => {
8
+ COLUMNS[fq] = short;
9
+ TYPE_VALIDATION[fq] = {
10
+ MYSQL_TYPE: 'VARCHAR(255)',
11
+ MAX_LENGTH: '255',
12
+ AUTO_INCREMENT: false,
13
+ SKIP_COLUMN_IN_POST: false,
14
+ };
15
+ });
16
+
17
+ // Derive primary keys: any short column ending with '_id'
18
+ const pkShorts = Object.values(columns)
19
+ .map(v => String(v))
20
+ .filter(v => v.toLowerCase().endsWith('_id')) as any[];
21
+ const pkFull = Object.entries(columns)
22
+ .filter(([, short]) => String(short).toLowerCase().endsWith('_id'))
23
+ .map(([fq]) => fq as any);
24
+
25
+ return {
26
+ TABLE_NAME: name,
27
+ PRIMARY: pkFull,
28
+ PRIMARY_SHORT: pkShorts as any,
29
+ COLUMNS,
30
+ TYPE_VALIDATION,
31
+ REGEX_VALIDATION: {},
32
+ LIFECYCLE_HOOKS: { GET: {}, POST: {}, PUT: {}, DELETE: {} } as any,
33
+ TABLE_REFERENCES: {},
34
+ TABLE_REFERENCED_BY: {},
35
+ // Uppercase fields — not used by builders but required by type
36
+ ID: undefined as any,
37
+ } as any;
38
+ }
39
+
40
+ export function buildTestConfig() {
41
+ const actorCols = {
42
+ 'actor.actor_id': 'actor_id',
43
+ 'actor.first_name': 'first_name',
44
+ 'actor.last_name': 'last_name',
45
+ 'actor.binarycol': 'binarycol',
46
+ } as const;
47
+
48
+ const filmActorCols = {
49
+ 'film_actor.actor_id': 'actor_id',
50
+ 'film_actor.film_id': 'film_id',
51
+ } as const;
52
+
53
+ const C6 = {
54
+ C6VERSION: 'test',
55
+ TABLES: {
56
+ actor: tableModel<'actor' & any>('actor', actorCols as any),
57
+ film_actor: tableModel<'film_actor' & any>('film_actor', filmActorCols as any),
58
+ },
59
+ PREFIX: '',
60
+ ORM: {} as any,
61
+ } as any;
62
+
63
+ // Special-case: mark binary column as BINARY to test conversion
64
+ C6.TABLES.actor.TYPE_VALIDATION['actor.binarycol'].MYSQL_TYPE = 'BINARY(16)';
65
+
66
+ const baseConfig: iRest<any, any, any> = {
67
+ C6,
68
+ restModel: C6.TABLES.actor,
69
+ requestMethod: 'GET',
70
+ verbose: false,
71
+ } as any;
72
+
73
+ return baseConfig;
74
+ }
@@ -0,0 +1,105 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { normalizeSingularRequest } from '../api/utils/normalizeSingularRequest';
3
+ import { C6C } from '../api/C6Constants';
4
+ import type { C6RestfulModel } from '../api/types/ormInterfaces';
5
+
6
+ function makeModel(table: string, pkShorts: string[], extraCols: string[] = []): C6RestfulModel<any, any, any> {
7
+ const COLUMNS: Record<string, string> = {};
8
+ const TYPE_VALIDATION: Record<string, any> = {};
9
+
10
+ // Always include PK columns as fully-qualified
11
+ for (const short of pkShorts) {
12
+ const fq = `${table}.${short}`;
13
+ COLUMNS[fq] = short;
14
+ TYPE_VALIDATION[fq] = { MYSQL_TYPE: 'INT', MAX_LENGTH: '11', AUTO_INCREMENT: false, SKIP_COLUMN_IN_POST: false };
15
+ }
16
+ // add extra simple columns
17
+ for (const short of extraCols) {
18
+ const fq = `${table}.${short}`;
19
+ COLUMNS[fq] = short;
20
+ TYPE_VALIDATION[fq] = { MYSQL_TYPE: 'VARCHAR(255)', MAX_LENGTH: '255', AUTO_INCREMENT: false, SKIP_COLUMN_IN_POST: false };
21
+ }
22
+
23
+ return {
24
+ TABLE_NAME: table,
25
+ PRIMARY: pkShorts.map(s => `${table}.${s}`) as any,
26
+ PRIMARY_SHORT: pkShorts as any,
27
+ COLUMNS: COLUMNS as any,
28
+ TYPE_VALIDATION,
29
+ REGEX_VALIDATION: {},
30
+ LIFECYCLE_HOOKS: { GET: {}, POST: {}, PUT: {}, DELETE: {} } as any,
31
+ TABLE_REFERENCES: {},
32
+ TABLE_REFERENCED_BY: {},
33
+ } as any;
34
+ }
35
+
36
+ describe('normalizeSingularRequest', () => {
37
+ it('converts GET singular T into WHERE by PK', () => {
38
+ const model = makeModel('actor', ['actor_id'], ['first_name']);
39
+ const req = { actor_id: 5 } as any;
40
+ const out = normalizeSingularRequest('GET', req, model);
41
+ expect(out).toHaveProperty(C6C.WHERE);
42
+ expect((out as any)[C6C.WHERE]).toEqual({ actor_id: 5 });
43
+ });
44
+
45
+ it('converts DELETE singular T into DELETE:true and WHERE by PK', () => {
46
+ const model = makeModel('actor', ['actor_id']);
47
+ const req = { actor_id: 7 } as any;
48
+ const out = normalizeSingularRequest('DELETE', req, model);
49
+ expect((out as any)[C6C.DELETE]).toBe(true);
50
+ expect((out as any)[C6C.WHERE]).toEqual({ actor_id: 7 });
51
+ });
52
+
53
+ it('converts PUT singular T into UPDATE (non-PK fields) and WHERE by PK', () => {
54
+ const model = makeModel('actor', ['actor_id'], ['first_name']);
55
+ const req = { actor_id: 9, first_name: 'NEW' } as any;
56
+ const out = normalizeSingularRequest('PUT', req, model);
57
+ expect((out as any)[C6C.WHERE]).toEqual({ actor_id: 9 });
58
+ expect((out as any)[C6C.UPDATE]).toEqual({ first_name: 'NEW' });
59
+ });
60
+
61
+ it('PUT singular T throws if no updatable fields beyond PK are present', () => {
62
+ const model = makeModel('actor', ['actor_id']);
63
+ const req = { actor_id: 3 } as any;
64
+ expect(() => normalizeSingularRequest('PUT', req, model)).toThrow(/must include at least one non-primary field to update/);
65
+ });
66
+
67
+ it('GET without PKs leaves request untouched (collection query)', () => {
68
+ const model = makeModel('actor', ['actor_id'], ['first_name']);
69
+ const req = { first_name: 'A' } as any;
70
+ const out = normalizeSingularRequest('GET', req, model) as any;
71
+ expect(out).toBe(req);
72
+ expect(out.first_name).toBe('A');
73
+ });
74
+
75
+ it('supports composite primary keys and requires all PKs', () => {
76
+ const model = makeModel('link', ['from_id', 'to_id']);
77
+ const ok = { from_id: 1, to_id: 2 } as any;
78
+ const out = normalizeSingularRequest('GET', ok, model);
79
+ expect((out as any)[C6C.WHERE]).toEqual({ from_id: 1, to_id: 2 });
80
+
81
+ const missing = { from_id: 1 } as any;
82
+ expect(() => normalizeSingularRequest('DELETE', missing, model)).toThrow(/Missing: \[to_id\]/);
83
+ });
84
+
85
+ it('GET with table that has no primary key leaves request untouched', () => {
86
+ const model = makeModel('nopk', [], ['name']);
87
+ const req = { name: 'X' } as any;
88
+ const out = normalizeSingularRequest('GET', req, model);
89
+ expect(out).toBe(req);
90
+ });
91
+
92
+ it('leaves already complex requests untouched', () => {
93
+ const model = makeModel('actor', ['actor_id']);
94
+ const complex = { [C6C.WHERE]: { actor_id: 1 }, [C6C.PAGINATION]: { LIMIT: 1 } } as any;
95
+ const out = normalizeSingularRequest('GET', complex, model);
96
+ expect(out).toBe(complex);
97
+ });
98
+
99
+ it('GET with only PAGINATION passes through unchanged', () => {
100
+ const model = makeModel('actor', ['actor_id']);
101
+ const req = { [C6C.PAGINATION]: { [C6C.PAGE]: 1, [C6C.LIMIT]: 100 } } as any;
102
+ const out = normalizeSingularRequest('GET', req, model);
103
+ expect(out).toBe(req);
104
+ });
105
+ });