@zerotal/orm 1.0.4 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -332,6 +332,19 @@ export class Blueprint {
332
332
  return this._add(new ColumnBuilder(name, "TEXT"));
333
333
  }
334
334
 
335
+ /**
336
+ * Alias of {@link Blueprint.dateTime}, spelled the way the column *type* is.
337
+ *
338
+ * The type string is lowercase (`@column({ type: "datetime" })`) while the builder
339
+ * method is camelCase, so reaching for `table.datetime(...)` is the natural mistake —
340
+ * and the blueprint is loosely typed, so it surfaced as a `TypeError` mid-migration
341
+ * rather than a compile error.
342
+ * @category Column types
343
+ */
344
+ datetime(name: string): ColumnBuilder {
345
+ return this.dateTime(name);
346
+ }
347
+
335
348
  /**
336
349
  * Timestamp column. Alias of {@link Blueprint.dateTime} — stored as `TEXT` (ISO-8601).
337
350
  * @category Column types
@@ -6,12 +6,23 @@ import { columnDbName, type ModelColumn, type ModelSchema } from "./ModelInspect
6
6
  // Maps @column({ type }) to the Blueprint method that generates the right SQL.
7
7
  const BLUEPRINT_METHOD: Record<string, string> = {
8
8
  string: "string",
9
+ text: "text",
9
10
  number: "integer",
10
11
  boolean: "boolean",
11
12
  datetime: "dateTime",
12
13
  json: "json",
13
14
  };
14
15
 
16
+ /**
17
+ * A column named `*_id` (or `*Id` before snake-casing) is a foreign key by convention,
18
+ * and an unindexed foreign key is a table scan on every join and every cascade check.
19
+ * The reference itself can't always be inferred — the target table is a guess — but the
20
+ * index can, and it is the half that matters for performance.
21
+ */
22
+ function _looksLikeForeignKey(dbName: string): boolean {
23
+ return dbName.endsWith("_id") && dbName !== "_id";
24
+ }
25
+
15
26
  // ── Code generation helpers ───────────────────────────────────────────────────
16
27
 
17
28
  function blueprintCall(col: ModelColumn, indent: string): string {
@@ -23,6 +34,26 @@ function blueprintCall(col: ModelColumn, indent: string): string {
23
34
  return line;
24
35
  }
25
36
 
37
+ /**
38
+ * The index lines for a table: everything declared via `@column({ unique | index })`,
39
+ * plus an inferred index on each foreign-key-shaped column that doesn't already have one.
40
+ */
41
+ function indexLines(columns: ModelColumn[], indent: string): string[] {
42
+ const lines: string[] = [];
43
+ for (const col of columns) {
44
+ if (col.primary) continue; // the PK is already indexed by increments()
45
+ const dbName = columnDbName(col.name);
46
+ if (col.unique) {
47
+ lines.push(`${indent}table.unique('${dbName}');`);
48
+ } else if (col.index) {
49
+ lines.push(`${indent}table.index('${dbName}');`);
50
+ } else if (_looksLikeForeignKey(dbName)) {
51
+ lines.push(`${indent}table.index('${dbName}');`);
52
+ }
53
+ }
54
+ return lines;
55
+ }
56
+
26
57
  function createTableBlock(schema: ModelSchema): string {
27
58
  const lines: string[] = [];
28
59
  lines.push(` await Schema.create('${schema.table}', (table) => {`);
@@ -36,6 +67,8 @@ function createTableBlock(schema: ModelSchema): string {
36
67
  if (schema.timestamps) lines.push(" table.timestamps();");
37
68
  if (schema.softDeletes) lines.push(" table.softDeletes();");
38
69
 
70
+ lines.push(...indexLines(schema.columns, " "));
71
+
39
72
  lines.push(" });");
40
73
  return lines.join("\n");
41
74
  }
@@ -57,6 +90,12 @@ function alterTableBlocks(newColumns: NewColumn[]): string {
57
90
  const lines: string[] = [];
58
91
  lines.push(` await Schema.table('${table}', (table) => {`);
59
92
  for (const nc of cols) lines.push(blueprintCall(nc.column, " "));
93
+ lines.push(
94
+ ...indexLines(
95
+ cols.map((nc) => nc.column),
96
+ " ",
97
+ ),
98
+ );
60
99
  lines.push(" });");
61
100
  blocks.push(lines.join("\n"));
62
101
  }
@@ -6,10 +6,14 @@ import { columnRegistry, columnsFor } from "../model/decorators/_metadata.ts";
6
6
 
7
7
  export interface ModelColumn {
8
8
  name: string;
9
- type: ColumnOptions["type"]; // 'string' | 'number' | 'boolean' | 'datetime' | 'json'
9
+ type: ColumnOptions["type"]; // 'string' | 'text' | 'number' | 'boolean' | 'datetime' | 'json'
10
10
  nullable: boolean;
11
11
  primary: boolean;
12
12
  default: unknown;
13
+ /** `@column({ unique: true })` — emit a unique index for this column. */
14
+ unique?: boolean;
15
+ /** `@column({ index: true })` — emit a plain index for this column. */
16
+ index?: boolean;
13
17
  }
14
18
 
15
19
  export interface ModelSchema {
@@ -59,6 +63,8 @@ function toModelColumns(fields: Map<string, ColumnOptions>): ModelColumn[] {
59
63
  nullable: opts.nullable ?? false,
60
64
  primary: opts.primary ?? false,
61
65
  default: opts.default,
66
+ unique: opts.unique ?? false,
67
+ index: opts.index ?? false,
62
68
  });
63
69
  }
64
70
  return columns;
@@ -101,6 +101,19 @@ export const Schema = {
101
101
  }
102
102
  },
103
103
 
104
+ /**
105
+ * Alias of {@link Schema.table}, for modifying an existing table.
106
+ *
107
+ * `alter` is the name Laravel and Knex use, so it is the first thing reached for — and
108
+ * because the blueprint callback is loosely typed, `Schema.alter(...)` was not a type
109
+ * error, only a `TypeError` at run time. A migration that fails there has already run
110
+ * whatever statements preceded it, leaving the schema half-changed, which is a worse
111
+ * outcome than one that never starts.
112
+ */
113
+ async alter(name: string, callback: (bp: Blueprint) => void): Promise<void> {
114
+ await Schema.table(name, callback);
115
+ },
116
+
104
117
  /** `DROP TABLE table_name` */
105
118
  async drop(table: string): Promise<void> {
106
119
  await ddl(`DROP TABLE ${table}`);
@@ -9,6 +9,7 @@ import type { ModelColumn } from "./ModelInspector.ts";
9
9
  // `migrate:generate` would - but applies it directly instead of writing a migration file.
10
10
  const BLUEPRINT_METHOD: Record<string, string> = {
11
11
  string: "string",
12
+ text: "text",
12
13
  number: "integer",
13
14
  boolean: "boolean",
14
15
  datetime: "dateTime",
@@ -21,15 +22,22 @@ type TableBuilder = Record<string, (name: string) => ColumnBuilder> & {
21
22
  timestamps(): void;
22
23
  softDeletes(): void;
23
24
  dropColumn(...names: string[]): unknown;
25
+ unique(columns: string | string[], name?: string): unknown;
26
+ index(columns: string | string[], name?: string): unknown;
24
27
  };
25
28
 
26
29
  function applyColumn(table: TableBuilder, col: ModelColumn): void {
27
30
  const method = BLUEPRINT_METHOD[col.type ?? "string"] ?? "string";
28
31
  // Models declare columns in camelCase; the ORM reads/writes snake_case — emit snake_case
29
32
  // so synchronize produces columns the runtime can actually read (e.g. two_factor_secret).
30
- const builder = table[method]!(columnDbName(col.name));
33
+ const dbName = columnDbName(col.name);
34
+ const builder = table[method]!(dbName);
31
35
  if (col.nullable) builder.nullable();
32
36
  if (col.default !== undefined) builder.default(col.default);
37
+ // Declared constraints travel with the column, so a synced schema carries the same
38
+ // uniqueness guarantee the model asserts rather than only the column's storage type.
39
+ if (col.unique) table.unique(dbName);
40
+ else if (col.index) table.index(dbName);
33
41
  }
34
42
 
35
43
  /** Options for {@link synchronizeSchema}. */