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.
@@ -6374,6 +6374,38 @@ var init_sqlTranslator = __esm({
6374
6374
  return sql;
6375
6375
  }
6376
6376
  }
6377
+ /**
6378
+ * Translate SQLite-canonical DDL column TYPES + CREATE-TABLE options to the
6379
+ * target engine.
6380
+ *
6381
+ * ONLY acts on `CREATE TABLE` / `ALTER TABLE` statements, so a query or INSERT
6382
+ * that happens to contain the word `TEXT` (a column name, a string literal) is
6383
+ * never rewritten. Complements `autoIncrementSyntax` (which maps the id
6384
+ * keyword) so ONE portable migration — and every `Model.createTable()` DDL,
6385
+ * which is also SQLite-canonical — applies on every engine instead of failing
6386
+ * on Firebird/MSSQL.
6387
+ *
6388
+ * * Firebird has no `TEXT` (-607), no `REAL`, and no `CREATE TABLE IF NOT
6389
+ * EXISTS`.
6390
+ * * MSSQL has no `CREATE TABLE IF NOT EXISTS` and its `TIMESTAMP` is a
6391
+ * rowversion, not a datetime — a `created_at TIMESTAMP` there is wrong.
6392
+ * * MySQL's `TIMESTAMP` carries auto-update / 2038 surprises, so a datetime
6393
+ * column maps to `DATETIME` (matching the adapters' createTableAsync).
6394
+ */
6395
+ static ddlTypes(sql, engine) {
6396
+ const head = sql.replace(/^(?:\s*--[^\n]*\n)+/, "");
6397
+ if (!/^\s*(?:CREATE\s+TABLE|ALTER\s+TABLE)\b/i.test(head)) return sql;
6398
+ switch ((engine ?? "").toLowerCase()) {
6399
+ case "firebird":
6400
+ 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");
6401
+ case "mssql":
6402
+ return sql.replace(/\bIF\s+NOT\s+EXISTS\b/gi, "").replace(/\bTIMESTAMP\b/gi, "DATETIME2");
6403
+ case "mysql":
6404
+ return sql.replace(/\bTIMESTAMP\b/gi, "DATETIME");
6405
+ default:
6406
+ return sql;
6407
+ }
6408
+ }
6377
6409
  /**
6378
6410
  * Convert ? placeholders to engine-specific style.
6379
6411
  *
@@ -8415,6 +8447,8 @@ var init_mysql = __esm({
8415
8447
  translateSql(sql) {
8416
8448
  let translated = SQLTranslator.concatPipesToFunc(sql);
8417
8449
  translated = SQLTranslator.ilikeToLike(translated);
8450
+ translated = SQLTranslator.autoIncrementSyntax(translated, "mysql");
8451
+ translated = SQLTranslator.ddlTypes(translated, "mysql");
8418
8452
  return translated;
8419
8453
  }
8420
8454
  execute(sql, params) {
@@ -8836,6 +8870,9 @@ var init_mssql = __esm({
8836
8870
  let translated = SQLTranslator.limitToTop(sql);
8837
8871
  translated = SQLTranslator.concatPipesToFunc(translated);
8838
8872
  translated = SQLTranslator.ilikeToLike(translated);
8873
+ translated = SQLTranslator.autoIncrementSyntax(translated, "mssql");
8874
+ translated = SQLTranslator.booleanToInt(translated);
8875
+ translated = SQLTranslator.ddlTypes(translated, "mssql");
8839
8876
  return translated;
8840
8877
  }
8841
8878
  execSqlPromise(sql, params) {
@@ -9466,6 +9503,8 @@ var init_firebird = __esm({
9466
9503
  let translated = SQLTranslator.limitToRows(sql);
9467
9504
  translated = SQLTranslator.booleanToInt(translated);
9468
9505
  translated = SQLTranslator.ilikeToLike(translated);
9506
+ translated = SQLTranslator.autoIncrementSyntax(translated, "firebird");
9507
+ translated = SQLTranslator.ddlTypes(translated, "firebird");
9469
9508
  return translated;
9470
9509
  }
9471
9510
  /**
@@ -15417,7 +15456,7 @@ function mulberry32(seed) {
15417
15456
  return ((t ^ t >>> 14) >>> 0) / 4294967296;
15418
15457
  };
15419
15458
  }
15420
- var FIRST_NAMES, LAST_NAMES, DOMAINS, WORDS, CITIES, COUNTRIES, STREETS, JOB_TITLES, CURRENCIES, FakeData;
15459
+ var FIRST_NAMES, LAST_NAMES, DOMAINS, WORDS, CITIES, COUNTRIES, STREETS, JOB_TITLES, CURRENCIES, PRODUCT_ADJECTIVES, PRODUCT_NOUNS, FakeData;
15421
15460
  var init_fakeData = __esm({
15422
15461
  "src/fakeData.ts"() {
15423
15462
  "use strict";
@@ -15679,6 +15718,59 @@ var init_fakeData = __esm({
15679
15718
  "ZAR",
15680
15719
  "INR"
15681
15720
  ];
15721
+ PRODUCT_ADJECTIVES = [
15722
+ "Wireless",
15723
+ "Organic",
15724
+ "Premium",
15725
+ "Classic",
15726
+ "Eco",
15727
+ "Smart",
15728
+ "Portable",
15729
+ "Deluxe",
15730
+ "Compact",
15731
+ "Rustic",
15732
+ "Handcrafted",
15733
+ "Vintage",
15734
+ "Modern",
15735
+ "Ergonomic",
15736
+ "Stainless",
15737
+ "Bamboo",
15738
+ "Recycled",
15739
+ "Artisan",
15740
+ "Professional",
15741
+ "Ultra",
15742
+ "Insulated",
15743
+ "Lightweight",
15744
+ "Adjustable",
15745
+ "Foldable"
15746
+ ];
15747
+ PRODUCT_NOUNS = [
15748
+ "Keyboard",
15749
+ "Coffee Beans",
15750
+ "Backpack",
15751
+ "Water Bottle",
15752
+ "Desk Lamp",
15753
+ "Headphones",
15754
+ "Notebook",
15755
+ "Sneakers",
15756
+ "Sunglasses",
15757
+ "Wallet",
15758
+ "Mug",
15759
+ "Chair",
15760
+ "Blender",
15761
+ "Speaker",
15762
+ "Charger",
15763
+ "Umbrella",
15764
+ "Toothbrush",
15765
+ "Jacket",
15766
+ "Watch",
15767
+ "Kettle",
15768
+ "Picture Frame",
15769
+ "Planter",
15770
+ "Cutlery Set",
15771
+ "Yoga Mat",
15772
+ "Phone Case"
15773
+ ];
15682
15774
  FakeData = class _FakeData {
15683
15775
  rng;
15684
15776
  seeded;
@@ -15750,6 +15842,16 @@ var init_fakeData = __esm({
15750
15842
  jobTitle() {
15751
15843
  return this.pick(JOB_TITLES);
15752
15844
  }
15845
+ /**
15846
+ * A plausible product name, e.g. "Wireless Keyboard" or "Organic Coffee
15847
+ * Beans" — an adjective + noun from the product vocabulary. Deterministic
15848
+ * under a seed like every other generator (draws from the same instance
15849
+ * PRNG). Used to seed a generic `name` column on a product-ish table instead
15850
+ * of a person name (see the ORM FakeData `forField` heuristic).
15851
+ */
15852
+ product() {
15853
+ return `${this.pick(PRODUCT_ADJECTIVES)} ${this.pick(PRODUCT_NOUNS)}`;
15854
+ }
15753
15855
  paragraph(sentences = 4) {
15754
15856
  const parts = [];
15755
15857
  for (let i = 0; i < sentences; i++) {
@@ -15873,11 +15975,27 @@ var init_fakeData = __esm({
15873
15975
  });
15874
15976
 
15875
15977
  // ../orm/src/fakeData.ts
15876
- var FakeData2;
15978
+ function isProductTable(table2) {
15979
+ const t = (table2 ?? "").toLowerCase();
15980
+ return PRODUCT_TABLE_HINTS.some((hint) => t.includes(hint));
15981
+ }
15982
+ var PRODUCT_TABLE_HINTS, FakeData2;
15877
15983
  var init_fakeData2 = __esm({
15878
15984
  "../orm/src/fakeData.ts"() {
15879
15985
  "use strict";
15880
15986
  init_fakeData();
15987
+ PRODUCT_TABLE_HINTS = [
15988
+ "product",
15989
+ "item",
15990
+ "catalog",
15991
+ "inventory",
15992
+ "goods",
15993
+ "merchandise",
15994
+ "sku",
15995
+ "listing",
15996
+ "stock",
15997
+ "ware"
15998
+ ];
15881
15999
  FakeData2 = class extends FakeData {
15882
16000
  constructor(seed) {
15883
16001
  super(seed);
@@ -15899,8 +16017,11 @@ var init_fakeData2 = __esm({
15899
16017
  *
15900
16018
  * @param fieldDef - An ORM FieldDefinition object
15901
16019
  * @param columnName - Optional column name for heuristic matching (e.g. "email", "phone")
16020
+ * @param table - Optional table/model name. A generic `name`/`full_name`
16021
+ * column on a product-ish table (see {@link isProductTable}) gets a
16022
+ * product name; with no table context it stays a person name (back-compat).
15902
16023
  */
15903
- forField(fieldDef, columnName) {
16024
+ forField(fieldDef, columnName, table2) {
15904
16025
  if (fieldDef.primaryKey && fieldDef.autoIncrement) {
15905
16026
  return void 0;
15906
16027
  }
@@ -15910,7 +16031,9 @@ var init_fakeData2 = __esm({
15910
16031
  const col = (columnName ?? "").toLowerCase();
15911
16032
  if (col.includes("email")) return this.email();
15912
16033
  if (col.includes("phone") || col.includes("mobile") || col.includes("tel")) return this.phone();
15913
- if (col === "name" || col === "full_name" || col === "fullname") return this.name();
16034
+ if (col === "name" || col === "full_name" || col === "fullname") {
16035
+ return isProductTable(table2) ? this.product() : this.name();
16036
+ }
15914
16037
  if (col === "first_name" || col === "firstname") return this.firstName();
15915
16038
  if (col === "last_name" || col === "lastname" || col === "surname") return this.lastName();
15916
16039
  if (col.includes("address")) return this.address();
@@ -15997,7 +16120,7 @@ async function autoFieldMap(db, table2, fake = new FakeData2()) {
15997
16120
  continue;
15998
16121
  }
15999
16122
  const fieldType = sqlTypeToFieldType(sqlType);
16000
- fieldMap[name] = () => fake.forField({ type: fieldType }, name);
16123
+ fieldMap[name] = () => fake.forField({ type: fieldType }, name, table2);
16001
16124
  }
16002
16125
  return fieldMap;
16003
16126
  }
@@ -16143,7 +16266,7 @@ async function seedOrm(ormClass, count = 10, overrides, seed, opts, fkPools) {
16143
16266
  } else if (pools[name] && pools[name].length > 0) {
16144
16267
  attrs[name] = fake.choice(pools[name]);
16145
16268
  } else {
16146
- attrs[name] = fake.forField(def, name);
16269
+ attrs[name] = fake.forField(def, name, modelName);
16147
16270
  }
16148
16271
  }
16149
16272
  validateTypes(fields, attrs, modelName);
@@ -24370,6 +24493,7 @@ __export(generate_exports, {
24370
24493
  parseEvery: () => parseEvery,
24371
24494
  parseFields: () => parseFields,
24372
24495
  pluralizeReserved: () => pluralizeReserved,
24496
+ resolveTable: () => resolveTable,
24373
24497
  toPascal: () => toPascal,
24374
24498
  toSnake: () => toSnake,
24375
24499
  toTableName: () => toTableName
@@ -24412,12 +24536,35 @@ function toTableName(name) {
24412
24536
  from: raw,
24413
24537
  to: safe,
24414
24538
  reason: `SQL reserved word '${raw}' would break CREATE TABLE`,
24415
- override: `--table ${raw} --quote (requires quoted-identifier mode, not yet implemented)`
24539
+ override: `--table-name <name> (table names interpolate unquoted; forcing a reserved name is yours to quote in raw SQL)`
24416
24540
  });
24417
24541
  return safe;
24418
24542
  }
24419
24543
  return raw;
24420
24544
  }
24545
+ function resolveTable(name, flags, opts = {}) {
24546
+ const announce = opts.announce ?? false;
24547
+ const override = (flags ?? {})["table-name"];
24548
+ if (typeof override === "string" && override) {
24549
+ if (announce && SQL_RESERVED_TABLE_NAMES.has(toSnake(override))) {
24550
+ console.error(
24551
+ ` ! 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.`
24552
+ );
24553
+ }
24554
+ return override;
24555
+ }
24556
+ const bare = toSnake(name);
24557
+ if (!SQL_RESERVED_TABLE_NAMES.has(bare)) {
24558
+ return bare;
24559
+ }
24560
+ const table2 = toTableName(name);
24561
+ if (announce) {
24562
+ console.error(
24563
+ ` \xB7 '${bare}' is a SQL reserved word; using table_name '${table2}' (Tina4 interpolates table names unquoted). Override with --table-name <name>.`
24564
+ );
24565
+ }
24566
+ return table2;
24567
+ }
24421
24568
  function resetResolution(target, input, opts) {
24422
24569
  __resolution.target = target;
24423
24570
  __resolution.input = input;
@@ -24516,10 +24663,11 @@ function printResolution() {
24516
24663
  lines.push(` migration ${b.migration_path}`);
24517
24664
  }
24518
24665
  const reserved = b.transformations.find((t) => t.kind === "reserved_word_pluralize");
24519
- if (reserved && reserved.from && reserved.override) {
24666
+ if (reserved && reserved.from) {
24520
24667
  lines.push("");
24521
- lines.push(` To keep the raw name '${reserved.from}' as the table:`);
24522
- lines.push(` tina4nodejs generate ${__resolution.target} ${__resolution.input.name} ${reserved.override}`);
24668
+ lines.push(` To set the table name yourself:`);
24669
+ lines.push(` tina4nodejs generate ${__resolution.target} ${__resolution.input.name} --table-name <name>`);
24670
+ lines.push(` Tina4 interpolates table names unquoted; if you force the reserved '${reserved.from}', you own the quoting in raw SQL.`);
24523
24671
  }
24524
24672
  if (b.test_paths && b.test_paths.length > 0) {
24525
24673
  lines.push("");
@@ -24655,6 +24803,7 @@ async function generate(what, name, extraArgs = []) {
24655
24803
  console.error(" Usage: tina4nodejs generate <what> <name> [options]");
24656
24804
  console.error(` Generators: ${GENERATOR_LIST}`);
24657
24805
  console.error(' Options: --fields "name:string,price:float" --model ModelName');
24806
+ console.error(" --table-name <name> force the model's table name (else derived; reserved words auto-pluralise)");
24658
24807
  console.error(" --public open a route's writes (default: secure)");
24659
24808
  console.error(' --every 5m | --cron "\u2026" service schedule');
24660
24809
  console.error(" --json emit machine-readable resolution envelope on stdout");
@@ -24705,7 +24854,7 @@ async function generateProgrammatic(what, name, extraArgs = []) {
24705
24854
  }
24706
24855
  function generateModel(name, flags, emitTest = true) {
24707
24856
  const fields = fieldsOrDefault(flags.fields || "");
24708
- const table2 = toTableName(name);
24857
+ const table2 = resolveTable(name, flags, { announce: true });
24709
24858
  const dir = resolve10("src/models");
24710
24859
  ensureDir(dir);
24711
24860
  const path8 = join20(dir, `${name}.ts`);
@@ -24756,7 +24905,7 @@ function generateRoute(name, flags, emitTest = true) {
24756
24905
  if (__resolution.target === "route") {
24757
24906
  setResolutionField("file_path", `src/routes/api/${routePath}/get.ts`);
24758
24907
  }
24759
- const table2 = model ? toTableName(model) : "";
24908
+ const table2 = model ? resolveTable(model, flags, { announce: false }) : "";
24760
24909
  const modelImportBase = model ? `import ${model} from "../../../models/${model}.js";
24761
24910
  ` : "";
24762
24911
  const modelImportId = model ? `import ${model} from "../../../../models/${model}.js";
@@ -24977,7 +25126,7 @@ ${aiFill(`delete_${singular}`, {
24977
25126
  }
24978
25127
  }
24979
25128
  function generateCrud(name, flags) {
24980
- const table2 = toTableName(name);
25129
+ const table2 = resolveTable(name, flags, { announce: false });
24981
25130
  const routeName = toPlural(table2);
24982
25131
  const isPublic = Boolean(flags.public);
24983
25132
  if (!__resolution.jsonMode) console.log(`
@@ -25004,7 +25153,7 @@ function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest
25004
25153
  table2 = tableOverride;
25005
25154
  } else {
25006
25155
  const raw = name.replace(/^create_/, "").replace(/^add_/, "").replace(/^drop_/, "");
25007
- table2 = toTableName(raw);
25156
+ table2 = resolveTable(raw, flags, { announce: false });
25008
25157
  }
25009
25158
  if (__resolution.target === "migration") {
25010
25159
  setResolutionField("table_name", table2);
@@ -25027,7 +25176,7 @@ function generateMigration(name, flags, fieldsOverride, tableOverride, emitTest
25027
25176
  const defaultClause = info.defaultVal !== "NULL" ? ` DEFAULT ${info.defaultVal}` : "";
25028
25177
  colLines.push(` ${fname} ${info.sql}${defaultClause}`);
25029
25178
  }
25030
- colLines.push(" created_at TEXT DEFAULT CURRENT_TIMESTAMP");
25179
+ colLines.push(" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP");
25031
25180
  upSql = `CREATE TABLE IF NOT EXISTS ${table2} (
25032
25181
  -- tina4:edit add columns beyond id + created_at
25033
25182
  ${colLines.join(",\n")}
@@ -25230,7 +25379,7 @@ void test${titleName};
25230
25379
  }
25231
25380
  function generateForm(name, flags) {
25232
25381
  const fields = fieldsOrDefault(flags.fields || "");
25233
- const table2 = toTableName(name);
25382
+ const table2 = resolveTable(name, flags, { announce: false });
25234
25383
  const routeName = toPlural(table2);
25235
25384
  const inputTypes = {
25236
25385
  string: "text",
@@ -25296,7 +25445,7 @@ function generateForm(name, flags) {
25296
25445
  }
25297
25446
  function generateView(name, flags) {
25298
25447
  const fields = fieldsOrDefault(flags.fields || "");
25299
- const table2 = toTableName(name);
25448
+ const table2 = resolveTable(name, flags, { announce: false });
25300
25449
  const routeName = toPlural(table2);
25301
25450
  const cols = fields.map(([f]) => f);
25302
25451
  const dir = resolve10("src/templates/pages");
@@ -25651,8 +25800,8 @@ ${rules} validator.required("name"); // starter rule (matches the model's def
25651
25800
  writeFileSafe(path8, content);
25652
25801
  emitValidatorTest(name, toSnake(name), toPascal(name));
25653
25802
  }
25654
- function generateSeeder(name, _flags) {
25655
- const table2 = toTableName(name);
25803
+ function generateSeeder(name, flags) {
25804
+ const table2 = resolveTable(name, flags, { announce: false });
25656
25805
  const dir = resolve10("src/seeds");
25657
25806
  ensureDir(dir);
25658
25807
  const path8 = join20(dir, `${table2}_seeder.ts`);
@@ -26170,8 +26319,8 @@ var init_generate = __esm({
26170
26319
  "../cli/src/commands/generate.ts"() {
26171
26320
  "use strict";
26172
26321
  FIELD_TYPE_MAP = {
26173
- string: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
26174
- str: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
26322
+ string: { orm: '"string"', sql: "VARCHAR(255)", defaultVal: "''" },
26323
+ str: { orm: '"string"', sql: "VARCHAR(255)", defaultVal: "''" },
26175
26324
  int: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
26176
26325
  integer: { orm: '"integer"', sql: "INTEGER", defaultVal: "0" },
26177
26326
  float: { orm: '"number"', sql: "REAL", defaultVal: "0" },
@@ -26181,7 +26330,7 @@ var init_generate = __esm({
26181
26330
  bool: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
26182
26331
  boolean: { orm: '"boolean"', sql: "INTEGER", defaultVal: "0" },
26183
26332
  text: { orm: '"string"', sql: "TEXT", defaultVal: "''" },
26184
- datetime: { orm: '"datetime"', sql: "TEXT", defaultVal: "NULL" },
26333
+ datetime: { orm: '"datetime"', sql: "TIMESTAMP", defaultVal: "NULL" },
26185
26334
  blob: { orm: '"string"', sql: "BLOB", defaultVal: "NULL" }
26186
26335
  };
26187
26336
  SQL_RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set([
@@ -26277,7 +26426,7 @@ var init_generate = __esm({
26277
26426
  TINA4_EDIT_MARKER = /^\s*(?:\/\/|--|\{#|#)\s*tina4:edit\s+(.+?)(?:\s*#\})?\s*$/;
26278
26427
  DEFAULT_FIELDS = [["name", "string"]];
26279
26428
  GENERATORS = {
26280
- model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"]', summary: "ORM model + matching migration" },
26429
+ model: { handler: generateModel, usage: '<Name> [--fields "name:string,price:float"] [--table-name <name>]', summary: "ORM model + matching migration" },
26281
26430
  route: { handler: generateRoute, usage: "<name> [--model Name] [--public]", summary: "CRUD route file, secure by default (--public opens writes)" },
26282
26431
  crud: { handler: generateCrud, usage: '<Name> [--fields "..."] [--public]', summary: "Model + migration + routes + form + view + test" },
26283
26432
  migration: { handler: (n, f) => generateMigration(n, f, void 0, void 0, !f["no-test"]), usage: "<description>", summary: "Timestamped migration file (UP/DOWN)" },
@@ -22,7 +22,10 @@ import { resolve, join } from "node:path";
22
22
 
23
23
  // ── Word Banks ───────────────────────────────────────────────────
24
24
 
25
- const FIRST_NAMES = [
25
+ // Exported (not via the barrel) so seeding tests can build the disjoint
26
+ // product-vs-person vocabulary check, mirroring the Python master's
27
+ // `_FIRST_NAMES` import. Internal-but-importable, like Python's underscore name.
28
+ export const FIRST_NAMES = [
26
29
  "Alice", "Bob", "Charlie", "Diana", "Eve", "Frank", "Grace", "Henry",
27
30
  "Ivy", "Jack", "Kate", "Leo", "Mia", "Noah", "Olivia", "Pete",
28
31
  "Quinn", "Rose", "Sam", "Tina", "Uma", "Vince", "Wendy", "Xander",
@@ -32,7 +35,7 @@ const FIRST_NAMES = [
32
35
  "Zara", "Amber", "Blake", "Clara",
33
36
  ];
34
37
 
35
- const LAST_NAMES = [
38
+ export const LAST_NAMES = [
36
39
  "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller",
37
40
  "Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez", "Wilson",
38
41
  "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin", "Lee",
@@ -88,6 +91,27 @@ const CURRENCIES = [
88
91
  "SEK", "NZD", "MXN", "SGD", "HKD", "NOK", "ZAR", "INR",
89
92
  ];
90
93
 
94
+ // Product-name vocabulary (adjective + noun). Seeds a generic `name` column on
95
+ // a product-ish table with "Wireless Keyboard" instead of a person name.
96
+ // Exported (not via the barrel) so seeding tests can assert product-vs-person
97
+ // disjointness — mirrors the Python master's `_PRODUCT_ADJECTIVES`. The two
98
+ // vocabularies are disjoint from FIRST_NAMES by construction, so the FIRST word
99
+ // of a generated value tells which generator ran.
100
+ export const PRODUCT_ADJECTIVES = [
101
+ "Wireless", "Organic", "Premium", "Classic", "Eco", "Smart", "Portable",
102
+ "Deluxe", "Compact", "Rustic", "Handcrafted", "Vintage", "Modern",
103
+ "Ergonomic", "Stainless", "Bamboo", "Recycled", "Artisan", "Professional",
104
+ "Ultra", "Insulated", "Lightweight", "Adjustable", "Foldable",
105
+ ];
106
+
107
+ export const PRODUCT_NOUNS = [
108
+ "Keyboard", "Coffee Beans", "Backpack", "Water Bottle", "Desk Lamp",
109
+ "Headphones", "Notebook", "Sneakers", "Sunglasses", "Wallet", "Mug",
110
+ "Chair", "Blender", "Speaker", "Charger", "Umbrella", "Toothbrush",
111
+ "Jacket", "Watch", "Kettle", "Picture Frame", "Planter", "Cutlery Set",
112
+ "Yoga Mat", "Phone Case",
113
+ ];
114
+
91
115
  // ── Seeded PRNG (mulberry32) ─────────────────────────────────────
92
116
 
93
117
  /**
@@ -193,6 +217,17 @@ export class FakeData {
193
217
  return this.pick(JOB_TITLES);
194
218
  }
195
219
 
220
+ /**
221
+ * A plausible product name, e.g. "Wireless Keyboard" or "Organic Coffee
222
+ * Beans" — an adjective + noun from the product vocabulary. Deterministic
223
+ * under a seed like every other generator (draws from the same instance
224
+ * PRNG). Used to seed a generic `name` column on a product-ish table instead
225
+ * of a person name (see the ORM FakeData `forField` heuristic).
226
+ */
227
+ product(): string {
228
+ return `${this.pick(PRODUCT_ADJECTIVES)} ${this.pick(PRODUCT_NOUNS)}`;
229
+ }
230
+
196
231
  paragraph(sentences = 4): string {
197
232
  const parts: string[] = [];
198
233
  for (let i = 0; i < sentences; i++) {