tina4-nodejs 3.13.124 → 3.13.130
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/CLAUDE.md +2 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +455 -85
- package/packages/cli/src/bin.ts +6 -0
- package/packages/cli/src/commands/generate.ts +93 -19
- package/packages/cli/src/commands/lint.ts +360 -0
- package/packages/core/dist/index.js +172 -23
- package/packages/core/src/fakeData.ts +37 -2
- package/packages/orm/dist/index.js +172 -23
- package/packages/orm/src/adapters/firebird.ts +7 -0
- package/packages/orm/src/adapters/mssql.ts +10 -0
- package/packages/orm/src/adapters/mysql.ts +6 -0
- package/packages/orm/src/fakeData.ts +27 -2
- package/packages/orm/src/seeder.ts +8 -3
- package/packages/orm/src/sqlTranslator.ts +45 -0
- package/types/cli/src/commands/generate.d.ts +29 -0
- package/types/cli/src/commands/lint.d.ts +5 -0
- package/types/core/src/fakeData.d.ts +12 -0
- package/types/orm/src/fakeData.d.ts +12 -1
- package/types/orm/src/sqlTranslator.d.ts +19 -0
|
@@ -915,6 +915,38 @@ var init_sqlTranslator = __esm({
|
|
|
915
915
|
return sql;
|
|
916
916
|
}
|
|
917
917
|
}
|
|
918
|
+
/**
|
|
919
|
+
* Translate SQLite-canonical DDL column TYPES + CREATE-TABLE options to the
|
|
920
|
+
* target engine.
|
|
921
|
+
*
|
|
922
|
+
* ONLY acts on `CREATE TABLE` / `ALTER TABLE` statements, so a query or INSERT
|
|
923
|
+
* that happens to contain the word `TEXT` (a column name, a string literal) is
|
|
924
|
+
* never rewritten. Complements `autoIncrementSyntax` (which maps the id
|
|
925
|
+
* keyword) so ONE portable migration — and every `Model.createTable()` DDL,
|
|
926
|
+
* which is also SQLite-canonical — applies on every engine instead of failing
|
|
927
|
+
* on Firebird/MSSQL.
|
|
928
|
+
*
|
|
929
|
+
* * Firebird has no `TEXT` (-607), no `REAL`, and no `CREATE TABLE IF NOT
|
|
930
|
+
* EXISTS`.
|
|
931
|
+
* * MSSQL has no `CREATE TABLE IF NOT EXISTS` and its `TIMESTAMP` is a
|
|
932
|
+
* rowversion, not a datetime — a `created_at TIMESTAMP` there is wrong.
|
|
933
|
+
* * MySQL's `TIMESTAMP` carries auto-update / 2038 surprises, so a datetime
|
|
934
|
+
* column maps to `DATETIME` (matching the adapters' createTableAsync).
|
|
935
|
+
*/
|
|
936
|
+
static ddlTypes(sql, engine) {
|
|
937
|
+
const head = sql.replace(/^(?:\s*--[^\n]*\n)+/, "");
|
|
938
|
+
if (!/^\s*(?:CREATE\s+TABLE|ALTER\s+TABLE)\b/i.test(head)) return sql;
|
|
939
|
+
switch ((engine ?? "").toLowerCase()) {
|
|
940
|
+
case "firebird":
|
|
941
|
+
return sql.replace(/\bIF\s+NOT\s+EXISTS\b/gi, "").replace(/\bBLOB\s+SUB_TYPE\s+TEXT\b/gi, "\0FBTEXT\0").replace(/\bTEXT\b/gi, "BLOB SUB_TYPE TEXT").replaceAll("\0FBTEXT\0", "BLOB SUB_TYPE TEXT").replace(/\bREAL\b/gi, "DOUBLE PRECISION");
|
|
942
|
+
case "mssql":
|
|
943
|
+
return sql.replace(/\bIF\s+NOT\s+EXISTS\b/gi, "").replace(/\bTIMESTAMP\b/gi, "DATETIME2");
|
|
944
|
+
case "mysql":
|
|
945
|
+
return sql.replace(/\bTIMESTAMP\b/gi, "DATETIME");
|
|
946
|
+
default:
|
|
947
|
+
return sql;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
918
950
|
/**
|
|
919
951
|
* Convert ? placeholders to engine-specific style.
|
|
920
952
|
*
|
|
@@ -12729,6 +12761,7 @@ __export(generate_exports, {
|
|
|
12729
12761
|
parseEvery: () => parseEvery,
|
|
12730
12762
|
parseFields: () => parseFields,
|
|
12731
12763
|
pluralizeReserved: () => pluralizeReserved,
|
|
12764
|
+
resolveTable: () => resolveTable,
|
|
12732
12765
|
toPascal: () => toPascal,
|
|
12733
12766
|
toSnake: () => toSnake,
|
|
12734
12767
|
toTableName: () => toTableName
|
|
@@ -12771,12 +12804,35 @@ function toTableName(name) {
|
|
|
12771
12804
|
from: raw,
|
|
12772
12805
|
to: safe,
|
|
12773
12806
|
reason: `SQL reserved word '${raw}' would break CREATE TABLE`,
|
|
12774
|
-
override: `--table
|
|
12807
|
+
override: `--table-name <name> (table names interpolate unquoted; forcing a reserved name is yours to quote in raw SQL)`
|
|
12775
12808
|
});
|
|
12776
12809
|
return safe;
|
|
12777
12810
|
}
|
|
12778
12811
|
return raw;
|
|
12779
12812
|
}
|
|
12813
|
+
function resolveTable(name, flags, opts = {}) {
|
|
12814
|
+
const announce = opts.announce ?? false;
|
|
12815
|
+
const override = (flags ?? {})["table-name"];
|
|
12816
|
+
if (typeof override === "string" && override) {
|
|
12817
|
+
if (announce && SQL_RESERVED_TABLE_NAMES.has(toSnake(override))) {
|
|
12818
|
+
console.error(
|
|
12819
|
+
` ! table_name '${override}' is a SQL reserved word. Tina4 interpolates table names UNQUOTED, so the ORM's generated SQL will fail on it -- quote it yourself in raw SQL and migrations.`
|
|
12820
|
+
);
|
|
12821
|
+
}
|
|
12822
|
+
return override;
|
|
12823
|
+
}
|
|
12824
|
+
const bare = toSnake(name);
|
|
12825
|
+
if (!SQL_RESERVED_TABLE_NAMES.has(bare)) {
|
|
12826
|
+
return bare;
|
|
12827
|
+
}
|
|
12828
|
+
const table2 = toTableName(name);
|
|
12829
|
+
if (announce) {
|
|
12830
|
+
console.error(
|
|
12831
|
+
` \xB7 '${bare}' is a SQL reserved word; using table_name '${table2}' (Tina4 interpolates table names unquoted). Override with --table-name <name>.`
|
|
12832
|
+
);
|
|
12833
|
+
}
|
|
12834
|
+
return table2;
|
|
12835
|
+
}
|
|
12780
12836
|
function resetResolution(target, input, opts) {
|
|
12781
12837
|
__resolution.target = target;
|
|
12782
12838
|
__resolution.input = input;
|
|
@@ -12875,10 +12931,11 @@ function printResolution() {
|
|
|
12875
12931
|
lines.push(` migration ${b.migration_path}`);
|
|
12876
12932
|
}
|
|
12877
12933
|
const reserved = b.transformations.find((t) => t.kind === "reserved_word_pluralize");
|
|
12878
|
-
if (reserved && reserved.from
|
|
12934
|
+
if (reserved && reserved.from) {
|
|
12879
12935
|
lines.push("");
|
|
12880
|
-
lines.push(` To
|
|
12881
|
-
lines.push(` tina4nodejs generate ${__resolution.target} ${__resolution.input.name}
|
|
12936
|
+
lines.push(` To set the table name yourself:`);
|
|
12937
|
+
lines.push(` tina4nodejs generate ${__resolution.target} ${__resolution.input.name} --table-name <name>`);
|
|
12938
|
+
lines.push(` Tina4 interpolates table names unquoted; if you force the reserved '${reserved.from}', you own the quoting in raw SQL.`);
|
|
12882
12939
|
}
|
|
12883
12940
|
if (b.test_paths && b.test_paths.length > 0) {
|
|
12884
12941
|
lines.push("");
|
|
@@ -13014,6 +13071,7 @@ async function generate(what, name, extraArgs = []) {
|
|
|
13014
13071
|
console.error(" Usage: tina4nodejs generate <what> <name> [options]");
|
|
13015
13072
|
console.error(` Generators: ${GENERATOR_LIST}`);
|
|
13016
13073
|
console.error(' Options: --fields "name:string,price:float" --model ModelName');
|
|
13074
|
+
console.error(" --table-name <name> force the model's table name (else derived; reserved words auto-pluralise)");
|
|
13017
13075
|
console.error(" --public open a route's writes (default: secure)");
|
|
13018
13076
|
console.error(' --every 5m | --cron "\u2026" service schedule');
|
|
13019
13077
|
console.error(" --json emit machine-readable resolution envelope on stdout");
|
|
@@ -13064,7 +13122,7 @@ async function generateProgrammatic(what, name, extraArgs = []) {
|
|
|
13064
13122
|
}
|
|
13065
13123
|
function generateModel(name, flags, emitTest = true) {
|
|
13066
13124
|
const fields = fieldsOrDefault(flags.fields || "");
|
|
13067
|
-
const table2 =
|
|
13125
|
+
const table2 = resolveTable(name, flags, { announce: true });
|
|
13068
13126
|
const dir = resolve6("src/models");
|
|
13069
13127
|
ensureDir(dir);
|
|
13070
13128
|
const path8 = join15(dir, `${name}.ts`);
|
|
@@ -13115,7 +13173,7 @@ function generateRoute(name, flags, emitTest = true) {
|
|
|
13115
13173
|
if (__resolution.target === "route") {
|
|
13116
13174
|
setResolutionField("file_path", `src/routes/api/${routePath}/get.ts`);
|
|
13117
13175
|
}
|
|
13118
|
-
const table2 = model ?
|
|
13176
|
+
const table2 = model ? resolveTable(model, flags, { announce: false }) : "";
|
|
13119
13177
|
const modelImportBase = model ? `import ${model} from "../../../models/${model}.js";
|
|
13120
13178
|
` : "";
|
|
13121
13179
|
const modelImportId = model ? `import ${model} from "../../../../models/${model}.js";
|
|
@@ -13336,7 +13394,7 @@ ${aiFill(`delete_${singular}`, {
|
|
|
13336
13394
|
}
|
|
13337
13395
|
}
|
|
13338
13396
|
function generateCrud(name, flags) {
|
|
13339
|
-
const table2 =
|
|
13397
|
+
const table2 = resolveTable(name, flags, { announce: false });
|
|
13340
13398
|
const routeName = toPlural(table2);
|
|
13341
13399
|
const isPublic = Boolean(flags.public);
|
|
13342
13400
|
if (!__resolution.jsonMode) console.log(`
|
|
@@ -13363,7 +13421,7 @@ function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest
|
|
|
13363
13421
|
table2 = tableOverride;
|
|
13364
13422
|
} else {
|
|
13365
13423
|
const raw = name.replace(/^create_/, "").replace(/^add_/, "").replace(/^drop_/, "");
|
|
13366
|
-
table2 =
|
|
13424
|
+
table2 = resolveTable(raw, flags, { announce: false });
|
|
13367
13425
|
}
|
|
13368
13426
|
if (__resolution.target === "migration") {
|
|
13369
13427
|
setResolutionField("table_name", table2);
|
|
@@ -13386,7 +13444,7 @@ function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest
|
|
|
13386
13444
|
const defaultClause = info.defaultVal !== "NULL" ? ` DEFAULT ${info.defaultVal}` : "";
|
|
13387
13445
|
colLines.push(` ${fname} ${info.sql}${defaultClause}`);
|
|
13388
13446
|
}
|
|
13389
|
-
colLines.push(" created_at
|
|
13447
|
+
colLines.push(" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP");
|
|
13390
13448
|
upSql = `CREATE TABLE IF NOT EXISTS ${table2} (
|
|
13391
13449
|
-- tina4:edit add columns beyond id + created_at
|
|
13392
13450
|
${colLines.join(",\n")}
|
|
@@ -13589,7 +13647,7 @@ void test${titleName};
|
|
|
13589
13647
|
}
|
|
13590
13648
|
function generateForm(name, flags) {
|
|
13591
13649
|
const fields = fieldsOrDefault(flags.fields || "");
|
|
13592
|
-
const table2 =
|
|
13650
|
+
const table2 = resolveTable(name, flags, { announce: false });
|
|
13593
13651
|
const routeName = toPlural(table2);
|
|
13594
13652
|
const inputTypes = {
|
|
13595
13653
|
string: "text",
|
|
@@ -13655,7 +13713,7 @@ function generateForm(name, flags) {
|
|
|
13655
13713
|
}
|
|
13656
13714
|
function generateView(name, flags) {
|
|
13657
13715
|
const fields = fieldsOrDefault(flags.fields || "");
|
|
13658
|
-
const table2 =
|
|
13716
|
+
const table2 = resolveTable(name, flags, { announce: false });
|
|
13659
13717
|
const routeName = toPlural(table2);
|
|
13660
13718
|
const cols = fields.map(([f]) => f);
|
|
13661
13719
|
const dir = resolve6("src/templates/pages");
|
|
@@ -14010,8 +14068,8 @@ ${rules} validator.required("name"); // starter rule (matches the model's def
|
|
|
14010
14068
|
writeFileSafe(path8, content);
|
|
14011
14069
|
emitValidatorTest(name, toSnake(name), toPascal(name));
|
|
14012
14070
|
}
|
|
14013
|
-
function generateSeeder(name,
|
|
14014
|
-
const table2 =
|
|
14071
|
+
function generateSeeder(name, flags) {
|
|
14072
|
+
const table2 = resolveTable(name, flags, { announce: false });
|
|
14015
14073
|
const dir = resolve6("src/seeds");
|
|
14016
14074
|
ensureDir(dir);
|
|
14017
14075
|
const path8 = join15(dir, `${table2}_seeder.ts`);
|
|
@@ -14529,8 +14587,8 @@ var init_generate = __esm({
|
|
|
14529
14587
|
"../cli/src/commands/generate.ts"() {
|
|
14530
14588
|
"use strict";
|
|
14531
14589
|
FIELD_TYPE_MAP = {
|
|
14532
|
-
string: { orm: '"string"', sql: "
|
|
14533
|
-
str: { orm: '"string"', sql: "
|
|
14590
|
+
string: { orm: '"string"', sql: "VARCHAR(255)", defaultVal: "''" },
|
|
14591
|
+
str: { orm: '"string"', sql: "VARCHAR(255)", defaultVal: "''" },
|
|
14534
14592
|
int: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
|
|
14535
14593
|
integer: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
|
|
14536
14594
|
float: { orm: '"number"', sql: "REAL", defaultVal: "0" },
|
|
@@ -14540,7 +14598,7 @@ var init_generate = __esm({
|
|
|
14540
14598
|
bool: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
|
|
14541
14599
|
boolean: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
|
|
14542
14600
|
text: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
|
|
14543
|
-
datetime: { orm: '"datetime"', sql: "
|
|
14601
|
+
datetime: { orm: '"datetime"', sql: "TIMESTAMP", defaultVal: "NULL" },
|
|
14544
14602
|
blob: { orm: '"string"', sql: "BLOB", defaultVal: "NULL" }
|
|
14545
14603
|
};
|
|
14546
14604
|
SQL_RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set([
|
|
@@ -14636,7 +14694,7 @@ var init_generate = __esm({
|
|
|
14636
14694
|
TINA4_EDIT_MARKER = /^\s*(?:\/\/|--|\{#|#)\s*tina4:edit\s+(.+?)(?:\s*#\})?\s*$/;
|
|
14637
14695
|
DEFAULT_FIELDS = [["name", "string"]];
|
|
14638
14696
|
GENERATORS = {
|
|
14639
|
-
model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"]', summary: "ORM model + matching migration" },
|
|
14697
|
+
model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"] [--table-name <name>]', summary: "ORM model + matching migration" },
|
|
14640
14698
|
route: { handler: generateRoute, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" },
|
|
14641
14699
|
crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public]', summary: "Model + migration + routes + form + view + test" },
|
|
14642
14700
|
migration: { handler: (n, f) => generateMigration(n, f, void 0, void 0, !f["no-test"]), usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
|
|
@@ -28091,7 +28149,7 @@ function mulberry32(seed) {
|
|
|
28091
28149
|
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
28092
28150
|
};
|
|
28093
28151
|
}
|
|
28094
|
-
var FIRST_NAMES, LAST_NAMES, DOMAINS, WORDS, CITIES, COUNTRIES, STREETS, JOB_TITLES, CURRENCIES, FakeData;
|
|
28152
|
+
var FIRST_NAMES, LAST_NAMES, DOMAINS, WORDS, CITIES, COUNTRIES, STREETS, JOB_TITLES, CURRENCIES, PRODUCT_ADJECTIVES, PRODUCT_NOUNS, FakeData;
|
|
28095
28153
|
var init_fakeData = __esm({
|
|
28096
28154
|
"../core/src/fakeData.ts"() {
|
|
28097
28155
|
"use strict";
|
|
@@ -28353,6 +28411,59 @@ var init_fakeData = __esm({
|
|
|
28353
28411
|
"ZAR",
|
|
28354
28412
|
"INR"
|
|
28355
28413
|
];
|
|
28414
|
+
PRODUCT_ADJECTIVES = [
|
|
28415
|
+
"Wireless",
|
|
28416
|
+
"Organic",
|
|
28417
|
+
"Premium",
|
|
28418
|
+
"Classic",
|
|
28419
|
+
"Eco",
|
|
28420
|
+
"Smart",
|
|
28421
|
+
"Portable",
|
|
28422
|
+
"Deluxe",
|
|
28423
|
+
"Compact",
|
|
28424
|
+
"Rustic",
|
|
28425
|
+
"Handcrafted",
|
|
28426
|
+
"Vintage",
|
|
28427
|
+
"Modern",
|
|
28428
|
+
"Ergonomic",
|
|
28429
|
+
"Stainless",
|
|
28430
|
+
"Bamboo",
|
|
28431
|
+
"Recycled",
|
|
28432
|
+
"Artisan",
|
|
28433
|
+
"Professional",
|
|
28434
|
+
"Ultra",
|
|
28435
|
+
"Insulated",
|
|
28436
|
+
"Lightweight",
|
|
28437
|
+
"Adjustable",
|
|
28438
|
+
"Foldable"
|
|
28439
|
+
];
|
|
28440
|
+
PRODUCT_NOUNS = [
|
|
28441
|
+
"Keyboard",
|
|
28442
|
+
"Coffee Beans",
|
|
28443
|
+
"Backpack",
|
|
28444
|
+
"Water Bottle",
|
|
28445
|
+
"Desk Lamp",
|
|
28446
|
+
"Headphones",
|
|
28447
|
+
"Notebook",
|
|
28448
|
+
"Sneakers",
|
|
28449
|
+
"Sunglasses",
|
|
28450
|
+
"Wallet",
|
|
28451
|
+
"Mug",
|
|
28452
|
+
"Chair",
|
|
28453
|
+
"Blender",
|
|
28454
|
+
"Speaker",
|
|
28455
|
+
"Charger",
|
|
28456
|
+
"Umbrella",
|
|
28457
|
+
"Toothbrush",
|
|
28458
|
+
"Jacket",
|
|
28459
|
+
"Watch",
|
|
28460
|
+
"Kettle",
|
|
28461
|
+
"Picture Frame",
|
|
28462
|
+
"Planter",
|
|
28463
|
+
"Cutlery Set",
|
|
28464
|
+
"Yoga Mat",
|
|
28465
|
+
"Phone Case"
|
|
28466
|
+
];
|
|
28356
28467
|
FakeData = class _FakeData {
|
|
28357
28468
|
rng;
|
|
28358
28469
|
seeded;
|
|
@@ -28424,6 +28535,16 @@ var init_fakeData = __esm({
|
|
|
28424
28535
|
jobTitle() {
|
|
28425
28536
|
return this.pick(JOB_TITLES);
|
|
28426
28537
|
}
|
|
28538
|
+
/**
|
|
28539
|
+
* A plausible product name, e.g. "Wireless Keyboard" or "Organic Coffee
|
|
28540
|
+
* Beans" — an adjective + noun from the product vocabulary. Deterministic
|
|
28541
|
+
* under a seed like every other generator (draws from the same instance
|
|
28542
|
+
* PRNG). Used to seed a generic `name` column on a product-ish table instead
|
|
28543
|
+
* of a person name (see the ORM FakeData `forField` heuristic).
|
|
28544
|
+
*/
|
|
28545
|
+
product() {
|
|
28546
|
+
return `${this.pick(PRODUCT_ADJECTIVES)} ${this.pick(PRODUCT_NOUNS)}`;
|
|
28547
|
+
}
|
|
28427
28548
|
paragraph(sentences = 4) {
|
|
28428
28549
|
const parts = [];
|
|
28429
28550
|
for (let i = 0; i < sentences; i++) {
|
|
@@ -37310,6 +37431,8 @@ var init_mysql = __esm({
|
|
|
37310
37431
|
translateSql(sql) {
|
|
37311
37432
|
let translated = SQLTranslator.concatPipesToFunc(sql);
|
|
37312
37433
|
translated = SQLTranslator.ilikeToLike(translated);
|
|
37434
|
+
translated = SQLTranslator.autoIncrementSyntax(translated, "mysql");
|
|
37435
|
+
translated = SQLTranslator.ddlTypes(translated, "mysql");
|
|
37313
37436
|
return translated;
|
|
37314
37437
|
}
|
|
37315
37438
|
execute(sql, params) {
|
|
@@ -37731,6 +37854,9 @@ var init_mssql = __esm({
|
|
|
37731
37854
|
let translated = SQLTranslator.limitToTop(sql);
|
|
37732
37855
|
translated = SQLTranslator.concatPipesToFunc(translated);
|
|
37733
37856
|
translated = SQLTranslator.ilikeToLike(translated);
|
|
37857
|
+
translated = SQLTranslator.autoIncrementSyntax(translated, "mssql");
|
|
37858
|
+
translated = SQLTranslator.booleanToInt(translated);
|
|
37859
|
+
translated = SQLTranslator.ddlTypes(translated, "mssql");
|
|
37734
37860
|
return translated;
|
|
37735
37861
|
}
|
|
37736
37862
|
execSqlPromise(sql, params) {
|
|
@@ -38361,6 +38487,8 @@ var init_firebird = __esm({
|
|
|
38361
38487
|
let translated = SQLTranslator.limitToRows(sql);
|
|
38362
38488
|
translated = SQLTranslator.booleanToInt(translated);
|
|
38363
38489
|
translated = SQLTranslator.ilikeToLike(translated);
|
|
38490
|
+
translated = SQLTranslator.autoIncrementSyntax(translated, "firebird");
|
|
38491
|
+
translated = SQLTranslator.ddlTypes(translated, "firebird");
|
|
38364
38492
|
return translated;
|
|
38365
38493
|
}
|
|
38366
38494
|
/**
|
|
@@ -44300,11 +44428,27 @@ var init_baseModel = __esm({
|
|
|
44300
44428
|
});
|
|
44301
44429
|
|
|
44302
44430
|
// src/fakeData.ts
|
|
44303
|
-
|
|
44431
|
+
function isProductTable(table2) {
|
|
44432
|
+
const t = (table2 ?? "").toLowerCase();
|
|
44433
|
+
return PRODUCT_TABLE_HINTS.some((hint) => t.includes(hint));
|
|
44434
|
+
}
|
|
44435
|
+
var PRODUCT_TABLE_HINTS, FakeData2;
|
|
44304
44436
|
var init_fakeData2 = __esm({
|
|
44305
44437
|
"src/fakeData.ts"() {
|
|
44306
44438
|
"use strict";
|
|
44307
44439
|
init_fakeData();
|
|
44440
|
+
PRODUCT_TABLE_HINTS = [
|
|
44441
|
+
"product",
|
|
44442
|
+
"item",
|
|
44443
|
+
"catalog",
|
|
44444
|
+
"inventory",
|
|
44445
|
+
"goods",
|
|
44446
|
+
"merchandise",
|
|
44447
|
+
"sku",
|
|
44448
|
+
"listing",
|
|
44449
|
+
"stock",
|
|
44450
|
+
"ware"
|
|
44451
|
+
];
|
|
44308
44452
|
FakeData2 = class extends FakeData {
|
|
44309
44453
|
constructor(seed) {
|
|
44310
44454
|
super(seed);
|
|
@@ -44326,8 +44470,11 @@ var init_fakeData2 = __esm({
|
|
|
44326
44470
|
*
|
|
44327
44471
|
* @param fieldDef - An ORM FieldDefinition object
|
|
44328
44472
|
* @param columnName - Optional column name for heuristic matching (e.g. "email", "phone")
|
|
44473
|
+
* @param table - Optional table/model name. A generic `name`/`full_name`
|
|
44474
|
+
* column on a product-ish table (see {@link isProductTable}) gets a
|
|
44475
|
+
* product name; with no table context it stays a person name (back-compat).
|
|
44329
44476
|
*/
|
|
44330
|
-
forField(fieldDef, columnName) {
|
|
44477
|
+
forField(fieldDef, columnName, table2) {
|
|
44331
44478
|
if (fieldDef.primaryKey && fieldDef.autoIncrement) {
|
|
44332
44479
|
return void 0;
|
|
44333
44480
|
}
|
|
@@ -44337,7 +44484,9 @@ var init_fakeData2 = __esm({
|
|
|
44337
44484
|
const col = (columnName ?? "").toLowerCase();
|
|
44338
44485
|
if (col.includes("email")) return this.email();
|
|
44339
44486
|
if (col.includes("phone") || col.includes("mobile") || col.includes("tel")) return this.phone();
|
|
44340
|
-
if (col === "name" || col === "full_name" || col === "fullname")
|
|
44487
|
+
if (col === "name" || col === "full_name" || col === "fullname") {
|
|
44488
|
+
return isProductTable(table2) ? this.product() : this.name();
|
|
44489
|
+
}
|
|
44341
44490
|
if (col === "first_name" || col === "firstname") return this.firstName();
|
|
44342
44491
|
if (col === "last_name" || col === "lastname" || col === "surname") return this.lastName();
|
|
44343
44492
|
if (col.includes("address")) return this.address();
|
|
@@ -44424,7 +44573,7 @@ async function autoFieldMap(db, table2, fake = new FakeData2()) {
|
|
|
44424
44573
|
continue;
|
|
44425
44574
|
}
|
|
44426
44575
|
const fieldType = sqlTypeToFieldType(sqlType);
|
|
44427
|
-
fieldMap[name] = () => fake.forField({ type: fieldType }, name);
|
|
44576
|
+
fieldMap[name] = () => fake.forField({ type: fieldType }, name, table2);
|
|
44428
44577
|
}
|
|
44429
44578
|
return fieldMap;
|
|
44430
44579
|
}
|
|
@@ -44570,7 +44719,7 @@ async function seedOrm(ormClass, count = 10, overrides, seed, opts, fkPools) {
|
|
|
44570
44719
|
} else if (pools[name] && pools[name].length > 0) {
|
|
44571
44720
|
attrs[name] = fake.choice(pools[name]);
|
|
44572
44721
|
} else {
|
|
44573
|
-
attrs[name] = fake.forField(def, name);
|
|
44722
|
+
attrs[name] = fake.forField(def, name, modelName);
|
|
44574
44723
|
}
|
|
44575
44724
|
}
|
|
44576
44725
|
validateTypes(fields, attrs, modelName);
|
|
@@ -395,6 +395,13 @@ export class FirebirdAdapter implements DatabaseAdapter {
|
|
|
395
395
|
let translated = SQLTranslator.limitToRows(sql);
|
|
396
396
|
translated = SQLTranslator.booleanToInt(translated);
|
|
397
397
|
translated = SQLTranslator.ilikeToLike(translated);
|
|
398
|
+
// DDL: strip AUTOINCREMENT (Firebird uses generators) and rewrite the
|
|
399
|
+
// SQLite-canonical column TYPES so ONE portable migration applies here —
|
|
400
|
+
// TEXT -> BLOB SUB_TYPE TEXT, REAL -> DOUBLE PRECISION, IF NOT EXISTS
|
|
401
|
+
// dropped. Both are DDL-only, so DML is untouched. Mirrors the Python
|
|
402
|
+
// master's firebird.py::_translate_sql.
|
|
403
|
+
translated = SQLTranslator.autoIncrementSyntax(translated, "firebird");
|
|
404
|
+
translated = SQLTranslator.ddlTypes(translated, "firebird");
|
|
398
405
|
return translated;
|
|
399
406
|
}
|
|
400
407
|
|
|
@@ -172,6 +172,16 @@ export class MssqlAdapter implements DatabaseAdapter {
|
|
|
172
172
|
let translated = SQLTranslator.limitToTop(sql);
|
|
173
173
|
translated = SQLTranslator.concatPipesToFunc(translated);
|
|
174
174
|
translated = SQLTranslator.ilikeToLike(translated);
|
|
175
|
+
// DDL: AUTOINCREMENT -> IDENTITY(1,1), drop IF NOT EXISTS (unsupported), and
|
|
176
|
+
// TIMESTAMP -> DATETIME2 (MSSQL's TIMESTAMP is a rowversion, not a datetime)
|
|
177
|
+
// so ONE portable migration applies here. Both are DDL-only, so DML is
|
|
178
|
+
// untouched. Mirrors the Python master's mssql.py::_translate_sql.
|
|
179
|
+
translated = SQLTranslator.autoIncrementSyntax(translated, "mssql");
|
|
180
|
+
// MSSQL has BIT, not a boolean type, so bare TRUE/FALSE must become 1/0
|
|
181
|
+
// (a TRUE/FALSE inside a string literal is data and is left untouched).
|
|
182
|
+
// Mirrors the Python master's mssql.py::_translate_sql.
|
|
183
|
+
translated = SQLTranslator.booleanToInt(translated);
|
|
184
|
+
translated = SQLTranslator.ddlTypes(translated, "mssql");
|
|
175
185
|
return translated;
|
|
176
186
|
}
|
|
177
187
|
|
|
@@ -148,6 +148,12 @@ export class MysqlAdapter implements DatabaseAdapter {
|
|
|
148
148
|
let translated = SQLTranslator.concatPipesToFunc(sql);
|
|
149
149
|
// MySQL uses LOWER() LIKE instead of ILIKE
|
|
150
150
|
translated = SQLTranslator.ilikeToLike(translated);
|
|
151
|
+
// DDL: AUTOINCREMENT -> AUTO_INCREMENT and TIMESTAMP -> DATETIME (MySQL's
|
|
152
|
+
// TIMESTAMP carries auto-update / 2038 surprises) so ONE portable migration
|
|
153
|
+
// applies here. Both are DDL-only, so DML is untouched. Mirrors the Python
|
|
154
|
+
// master's mysql.py::_translate_sql.
|
|
155
|
+
translated = SQLTranslator.autoIncrementSyntax(translated, "mysql");
|
|
156
|
+
translated = SQLTranslator.ddlTypes(translated, "mysql");
|
|
151
157
|
return translated;
|
|
152
158
|
}
|
|
153
159
|
|
|
@@ -5,6 +5,26 @@
|
|
|
5
5
|
import { FakeData as CoreFakeData } from "../../core/src/fakeData.js";
|
|
6
6
|
import type { FieldDefinition } from "./types.js";
|
|
7
7
|
|
|
8
|
+
// A table/model whose name contains any of these gets product names on its
|
|
9
|
+
// generic `name`/`full_name` column instead of a person name. Mirrors the
|
|
10
|
+
// Python master's `_PRODUCT_TABLE_HINTS`.
|
|
11
|
+
const PRODUCT_TABLE_HINTS = [
|
|
12
|
+
"product", "item", "catalog", "inventory", "goods", "merchandise",
|
|
13
|
+
"sku", "listing", "stock", "ware",
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* True when the table/model name looks like a product catalogue, so a generic
|
|
18
|
+
* `name` column should seed a product name, not a person name. With NO table
|
|
19
|
+
* context (undefined/null/empty) this is false, so the person-name default is
|
|
20
|
+
* kept — back-compat. Exported (not via the barrel) so seeding tests can assert
|
|
21
|
+
* it directly, mirroring the Python master's `_is_product_table`.
|
|
22
|
+
*/
|
|
23
|
+
export function isProductTable(table?: string | null): boolean {
|
|
24
|
+
const t = (table ?? "").toLowerCase();
|
|
25
|
+
return PRODUCT_TABLE_HINTS.some((hint) => t.includes(hint));
|
|
26
|
+
}
|
|
27
|
+
|
|
8
28
|
/**
|
|
9
29
|
* ORM-aware FakeData — wraps the core FakeData and adds forField()
|
|
10
30
|
* which generates appropriate fake data based on an ORM FieldDefinition.
|
|
@@ -32,8 +52,11 @@ export class FakeData extends CoreFakeData {
|
|
|
32
52
|
*
|
|
33
53
|
* @param fieldDef - An ORM FieldDefinition object
|
|
34
54
|
* @param columnName - Optional column name for heuristic matching (e.g. "email", "phone")
|
|
55
|
+
* @param table - Optional table/model name. A generic `name`/`full_name`
|
|
56
|
+
* column on a product-ish table (see {@link isProductTable}) gets a
|
|
57
|
+
* product name; with no table context it stays a person name (back-compat).
|
|
35
58
|
*/
|
|
36
|
-
forField(fieldDef: FieldDefinition, columnName?: string): unknown {
|
|
59
|
+
forField(fieldDef: FieldDefinition, columnName?: string, table?: string): unknown {
|
|
37
60
|
// Auto-increment primary keys should not be generated
|
|
38
61
|
if (fieldDef.primaryKey && fieldDef.autoIncrement) {
|
|
39
62
|
return undefined;
|
|
@@ -53,7 +76,9 @@ export class FakeData extends CoreFakeData {
|
|
|
53
76
|
|
|
54
77
|
if (col.includes("email")) return this.email();
|
|
55
78
|
if (col.includes("phone") || col.includes("mobile") || col.includes("tel")) return this.phone();
|
|
56
|
-
if (col === "name" || col === "full_name" || col === "fullname")
|
|
79
|
+
if (col === "name" || col === "full_name" || col === "fullname") {
|
|
80
|
+
return isProductTable(table) ? this.product() : this.name();
|
|
81
|
+
}
|
|
57
82
|
if (col === "first_name" || col === "firstname") return this.firstName();
|
|
58
83
|
if (col === "last_name" || col === "lastname" || col === "surname") return this.lastName();
|
|
59
84
|
if (col.includes("address")) return this.address();
|
|
@@ -143,8 +143,10 @@ export async function autoFieldMap(
|
|
|
143
143
|
}
|
|
144
144
|
const fieldType = sqlTypeToFieldType(sqlType);
|
|
145
145
|
// Bind the type + name per column; forField applies the name heuristics
|
|
146
|
-
// (email/phone/name/...) before falling back to the type.
|
|
147
|
-
|
|
146
|
+
// (email/phone/name/...) before falling back to the type. The TABLE name is
|
|
147
|
+
// threaded so a generic `name` column on a product-ish table seeds a
|
|
148
|
+
// product name, not a person name (parity with the Python auto_field_map).
|
|
149
|
+
fieldMap[name] = () => fake.forField({ type: fieldType }, name, table);
|
|
148
150
|
}
|
|
149
151
|
return fieldMap;
|
|
150
152
|
}
|
|
@@ -427,7 +429,10 @@ export async function seedOrm(
|
|
|
427
429
|
} else if (pools[name] && pools[name].length > 0) {
|
|
428
430
|
attrs[name] = fake.choice(pools[name]);
|
|
429
431
|
} else {
|
|
430
|
-
|
|
432
|
+
// Thread the MODEL name so a generic `name` column on a product-ish
|
|
433
|
+
// model seeds a product name, not a person name (Python seed_orm
|
|
434
|
+
// passes orm_class.__name__; modelName is name ?? tableName).
|
|
435
|
+
attrs[name] = fake.forField(def, name, modelName);
|
|
431
436
|
}
|
|
432
437
|
}
|
|
433
438
|
validateTypes(fields, attrs, modelName);
|
|
@@ -269,6 +269,51 @@ export class SQLTranslator {
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Translate SQLite-canonical DDL column TYPES + CREATE-TABLE options to the
|
|
274
|
+
* target engine.
|
|
275
|
+
*
|
|
276
|
+
* ONLY acts on `CREATE TABLE` / `ALTER TABLE` statements, so a query or INSERT
|
|
277
|
+
* that happens to contain the word `TEXT` (a column name, a string literal) is
|
|
278
|
+
* never rewritten. Complements `autoIncrementSyntax` (which maps the id
|
|
279
|
+
* keyword) so ONE portable migration — and every `Model.createTable()` DDL,
|
|
280
|
+
* which is also SQLite-canonical — applies on every engine instead of failing
|
|
281
|
+
* on Firebird/MSSQL.
|
|
282
|
+
*
|
|
283
|
+
* * Firebird has no `TEXT` (-607), no `REAL`, and no `CREATE TABLE IF NOT
|
|
284
|
+
* EXISTS`.
|
|
285
|
+
* * MSSQL has no `CREATE TABLE IF NOT EXISTS` and its `TIMESTAMP` is a
|
|
286
|
+
* rowversion, not a datetime — a `created_at TIMESTAMP` there is wrong.
|
|
287
|
+
* * MySQL's `TIMESTAMP` carries auto-update / 2038 surprises, so a datetime
|
|
288
|
+
* column maps to `DATETIME` (matching the adapters' createTableAsync).
|
|
289
|
+
*/
|
|
290
|
+
static ddlTypes(sql: string, engine: string): string {
|
|
291
|
+
// Gate to DDL only, tolerating leading `-- ...` comment lines / blank lines
|
|
292
|
+
// that a migration file carries before its CREATE TABLE. A SELECT or INSERT
|
|
293
|
+
// that merely mentions a type keyword is never rewritten.
|
|
294
|
+
const head = sql.replace(/^(?:\s*--[^\n]*\n)+/, "");
|
|
295
|
+
if (!/^\s*(?:CREATE\s+TABLE|ALTER\s+TABLE)\b/i.test(head)) return sql;
|
|
296
|
+
switch ((engine ?? "").toLowerCase()) {
|
|
297
|
+
case "firebird":
|
|
298
|
+
return sql
|
|
299
|
+
.replace(/\bIF\s+NOT\s+EXISTS\b/gi, "")
|
|
300
|
+
// Map bare TEXT -> BLOB SUB_TYPE TEXT, but leave an existing
|
|
301
|
+
// "BLOB SUB_TYPE TEXT" intact (it already contains the word TEXT).
|
|
302
|
+
.replace(/\bBLOB\s+SUB_TYPE\s+TEXT\b/gi, "\x00FBTEXT\x00")
|
|
303
|
+
.replace(/\bTEXT\b/gi, "BLOB SUB_TYPE TEXT")
|
|
304
|
+
.replaceAll("\x00FBTEXT\x00", "BLOB SUB_TYPE TEXT")
|
|
305
|
+
.replace(/\bREAL\b/gi, "DOUBLE PRECISION");
|
|
306
|
+
case "mssql":
|
|
307
|
+
return sql
|
|
308
|
+
.replace(/\bIF\s+NOT\s+EXISTS\b/gi, "")
|
|
309
|
+
.replace(/\bTIMESTAMP\b/gi, "DATETIME2");
|
|
310
|
+
case "mysql":
|
|
311
|
+
return sql.replace(/\bTIMESTAMP\b/gi, "DATETIME");
|
|
312
|
+
default:
|
|
313
|
+
return sql;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
272
317
|
/**
|
|
273
318
|
* Convert ? placeholders to engine-specific style.
|
|
274
319
|
*
|
|
@@ -10,6 +10,35 @@ export declare function pluralizeReserved(name: string): string;
|
|
|
10
10
|
* routes and tests all agree on the same table name.
|
|
11
11
|
*/
|
|
12
12
|
export declare function toTableName(name: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* The table name a generator uses (issue #123) — honours `--table-name` and
|
|
15
|
+
* speaks up instead of renaming SILENTLY. Mirrors the Python master's
|
|
16
|
+
* `_resolve_table` (tina4-python/tina4_python/cli/__init__.py).
|
|
17
|
+
*
|
|
18
|
+
* `announce` prints the note/warning; it is TRUE only for `generateModel` (where
|
|
19
|
+
* the table is born). Composite generators (crud) let the model sub-call
|
|
20
|
+
* announce, and generators that target an EXISTING table (route/seeder/form/view/
|
|
21
|
+
* migration) still honour `--table-name` but stay quiet so the note is not
|
|
22
|
+
* repeated — the note prints exactly once per `generate`.
|
|
23
|
+
*
|
|
24
|
+
* • `--table-name <name>` wins verbatim. If that name is ITSELF a reserved word,
|
|
25
|
+
* warn loudly (when announcing): Tina4 interpolates table names UNQUOTED, so
|
|
26
|
+
* the ORM's generated SQL will fail on it — quoting it in raw SQL + migrations
|
|
27
|
+
* is now the developer's job (we do NOT silently quote; identifier quoting is a
|
|
28
|
+
* global storage invariant, not a local fix).
|
|
29
|
+
* • Otherwise fall back to `toTableName` (snake + reserved-word pluralise). When
|
|
30
|
+
* that auto-pluralises a reserved-word class name (`Order` -> `orders`), print a
|
|
31
|
+
* one-line NOTE (when announcing) naming the rename and the `--table-name`
|
|
32
|
+
* escape hatch, so the developer is informed rather than surprised.
|
|
33
|
+
*
|
|
34
|
+
* The note/warning goes to STDERR (console.error) so a `generate … --json` run
|
|
35
|
+
* keeps its stdout envelope pristine for a downstream `| jq`. `toTableName`'s
|
|
36
|
+
* `reserved_word_pluralize` envelope transformation is UNCHANGED (this ADDS the
|
|
37
|
+
* announce path; it does not touch the envelope contract).
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveTable(name: string, flags: Record<string, string | boolean> | undefined, opts?: {
|
|
40
|
+
announce?: boolean;
|
|
41
|
+
}): string;
|
|
13
42
|
/** One transformation the resolver made — visible to the caller so an AI
|
|
14
43
|
* agent (or human) knows exactly why the output differs from the input. */
|
|
15
44
|
export interface ResolutionTransformation {
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export declare const FIRST_NAMES: string[];
|
|
2
|
+
export declare const LAST_NAMES: string[];
|
|
3
|
+
export declare const PRODUCT_ADJECTIVES: string[];
|
|
4
|
+
export declare const PRODUCT_NOUNS: string[];
|
|
1
5
|
export declare class FakeData {
|
|
2
6
|
private rng;
|
|
3
7
|
private seeded;
|
|
@@ -19,6 +23,14 @@ export declare class FakeData {
|
|
|
19
23
|
zipCode(): string;
|
|
20
24
|
company(): string;
|
|
21
25
|
jobTitle(): string;
|
|
26
|
+
/**
|
|
27
|
+
* A plausible product name, e.g. "Wireless Keyboard" or "Organic Coffee
|
|
28
|
+
* Beans" — an adjective + noun from the product vocabulary. Deterministic
|
|
29
|
+
* under a seed like every other generator (draws from the same instance
|
|
30
|
+
* PRNG). Used to seed a generic `name` column on a product-ish table instead
|
|
31
|
+
* of a person name (see the ORM FakeData `forField` heuristic).
|
|
32
|
+
*/
|
|
33
|
+
product(): string;
|
|
22
34
|
paragraph(sentences?: number): string;
|
|
23
35
|
sentence(words?: number): string;
|
|
24
36
|
word(): string;
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { FakeData as CoreFakeData } from "../../core/src/fakeData.js";
|
|
2
2
|
import type { FieldDefinition } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* True when the table/model name looks like a product catalogue, so a generic
|
|
5
|
+
* `name` column should seed a product name, not a person name. With NO table
|
|
6
|
+
* context (undefined/null/empty) this is false, so the person-name default is
|
|
7
|
+
* kept — back-compat. Exported (not via the barrel) so seeding tests can assert
|
|
8
|
+
* it directly, mirroring the Python master's `_is_product_table`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isProductTable(table?: string | null): boolean;
|
|
3
11
|
/**
|
|
4
12
|
* ORM-aware FakeData — wraps the core FakeData and adds forField()
|
|
5
13
|
* which generates appropriate fake data based on an ORM FieldDefinition.
|
|
@@ -17,6 +25,9 @@ export declare class FakeData extends CoreFakeData {
|
|
|
17
25
|
*
|
|
18
26
|
* @param fieldDef - An ORM FieldDefinition object
|
|
19
27
|
* @param columnName - Optional column name for heuristic matching (e.g. "email", "phone")
|
|
28
|
+
* @param table - Optional table/model name. A generic `name`/`full_name`
|
|
29
|
+
* column on a product-ish table (see {@link isProductTable}) gets a
|
|
30
|
+
* product name; with no table context it stays a person name (back-compat).
|
|
20
31
|
*/
|
|
21
|
-
forField(fieldDef: FieldDefinition, columnName?: string): unknown;
|
|
32
|
+
forField(fieldDef: FieldDefinition, columnName?: string, table?: string): unknown;
|
|
22
33
|
}
|