@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.
- package/README.md +2 -1
- package/dist/api/orm/builders/AggregateBuilder.d.ts +1 -0
- package/dist/api/types/ormInterfaces.d.ts +2 -2
- package/dist/api/utils/cacheManager.d.ts +1 -1
- package/dist/api/utils/normalizeSingularRequest.d.ts +10 -0
- package/dist/index.cjs.js +182 -32
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +183 -34
- package/dist/index.esm.js.map +1 -1
- package/package.json +11 -6
- package/scripts/assets/handlebars/C6.test.ts.handlebars +55 -80
- package/scripts/assets/handlebars/C6.ts.handlebars +28 -2
- package/scripts/generateRestBindings.cjs +17 -6
- package/scripts/generateRestBindings.ts +22 -8
- package/src/__tests__/fixtures/c6.fixture.ts +74 -0
- package/src/__tests__/normalizeSingularRequest.test.ts +105 -0
- package/src/__tests__/sakila-db/C6.js +1487 -0
- package/src/__tests__/sakila-db/C6.test.ts +63 -0
- package/src/__tests__/sakila-db/C6.ts +2206 -0
- package/src/__tests__/sakila-db/sakila-data.sql +46444 -0
- package/src/__tests__/sakila-db/sakila-schema.sql +686 -0
- package/src/__tests__/sakila-db/sakila.mwb +0 -0
- package/src/__tests__/sakila.generated.test.ts +46 -0
- package/src/__tests__/sqlBuilders.complex.test.ts +134 -0
- package/src/__tests__/sqlBuilders.test.ts +121 -0
- package/src/api/convertForRequestBody.ts +1 -1
- package/src/api/executors/HttpExecutor.ts +14 -3
- package/src/api/executors/SqlExecutor.ts +14 -1
- package/src/api/orm/builders/AggregateBuilder.ts +3 -0
- package/src/api/orm/builders/ConditionBuilder.ts +34 -11
- package/src/api/orm/builders/PaginationBuilder.ts +10 -4
- package/src/api/orm/queries/SelectQueryBuilder.ts +3 -0
- package/src/api/orm/queries/UpdateQueryBuilder.ts +2 -1
- package/src/api/types/ormInterfaces.ts +3 -4
- package/src/api/utils/cacheManager.ts +1 -1
- package/src/api/utils/normalizeSingularRequest.ts +144 -0
- 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'] || '
|
|
116
|
-
MySQLDump.DB_PREFIX = argMap['--prefix'] || '
|
|
117
|
-
MySQLDump.RELATIVE_OUTPUT_DIR = argMap['--output'] || '/src
|
|
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
|
-
|
|
409
|
-
fs.writeFileSync(path.join(
|
|
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'] || '
|
|
28
|
-
static DB_PREFIX = argMap['--prefix'] || '
|
|
29
|
-
static RELATIVE_OUTPUT_DIR = argMap['--output'] || '/src
|
|
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();
|
|
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
|
|
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
|
-
|
|
493
|
-
|
|
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
|
+
});
|