@ghom/orm 1.10.0 → 2.0.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.
package/dist/app/orm.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import url from "node:url";
2
2
  import { Handler } from "@ghom/handler";
3
+ import { CachedQuery } from "@ghom/query";
3
4
  import { default as knex } from "knex";
4
- import { isCJS } from "./util.js";
5
+ import { backupTable, disableForeignKeys, enableForeignKeys, restoreBackup } from "./backup.js";
5
6
  import { Table } from "./table.js";
6
- import { CachedQuery } from "@ghom/query";
7
- import { backupTable, restoreBackup, disableForeignKeys, enableForeignKeys, } from "./backup.js";
7
+ import { isCJS } from "./util.js";
8
8
  /**
9
9
  * The main ORM class that manages database connections, tables, and caching.
10
10
  *
@@ -24,7 +24,7 @@ import { backupTable, restoreBackup, disableForeignKeys, enableForeignKeys, } fr
24
24
  export class ORM {
25
25
  config;
26
26
  _ready = false;
27
- client;
27
+ _client;
28
28
  handler;
29
29
  _rawCache;
30
30
  /**
@@ -43,7 +43,7 @@ export class ORM {
43
43
  this.config = config;
44
44
  if (config === false)
45
45
  return;
46
- this.client = knex(config.database ?? {
46
+ this._client = knex(config.database ?? {
47
47
  client: "sqlite3",
48
48
  useNullAsDefault: true,
49
49
  connection: {
@@ -62,14 +62,18 @@ export class ORM {
62
62
  this._rawCache = new CachedQuery(async (raw) => await this.raw(raw), config.caching ?? Infinity);
63
63
  }
64
64
  requireClient() {
65
- if (!this.client)
65
+ if (!this._client)
66
66
  throw new Error("ORM client is not initialized. Cannot use this method without a database connection.");
67
67
  }
68
+ get client() {
69
+ this.requireClient();
70
+ return this._client;
71
+ }
68
72
  /**
69
73
  * Returns true if the ORM has a database client connected.
70
74
  */
71
75
  get isConnected() {
72
- return this.client !== undefined;
76
+ return this._client !== undefined;
73
77
  }
74
78
  get cachedTables() {
75
79
  if (!this.handler)
@@ -84,7 +88,7 @@ export class ORM {
84
88
  }
85
89
  async hasTable(name) {
86
90
  this.requireClient();
87
- return this.client.schema.hasTable(name);
91
+ return this._client.schema.hasTable(name);
88
92
  }
89
93
  /**
90
94
  * Handle the table files and create the tables in the database.
@@ -99,10 +103,10 @@ export class ORM {
99
103
  this.handler.elements.set("migration", new Table({
100
104
  name: "migration",
101
105
  priority: Infinity,
102
- setup: (table) => {
103
- table.string("table").unique().notNullable();
104
- table.integer("version").notNullable();
105
- },
106
+ columns: (col) => ({
107
+ table: col.string().unique(),
108
+ version: col.integer(),
109
+ }),
106
110
  }));
107
111
  const sortedTables = this.cachedTables.toSorted((a, b) => (b.options.priority ?? 0) - (a.options.priority ?? 0));
108
112
  for (const table of sortedTables) {
@@ -114,7 +118,7 @@ export class ORM {
114
118
  this.requireClient();
115
119
  if (this._ready)
116
120
  this.cache.invalidate();
117
- return this.client.raw(sql);
121
+ return this._client.raw(sql);
118
122
  }
119
123
  cache = {
120
124
  raw: (sql, anyDataUpdated) => {
@@ -140,7 +144,7 @@ export class ORM {
140
144
  */
141
145
  async createBackup(dirname) {
142
146
  this.requireClient();
143
- for (let table of this.cachedTables) {
147
+ for (const table of this.cachedTables) {
144
148
  await backupTable(table, dirname);
145
149
  }
146
150
  console.log("Database backup created.");
@@ -152,7 +156,7 @@ export class ORM {
152
156
  async restoreBackup(dirname) {
153
157
  this.requireClient();
154
158
  await disableForeignKeys(this, async (trx) => {
155
- for (let table of this.cachedTables) {
159
+ for (const table of this.cachedTables) {
156
160
  await restoreBackup(table, trx, dirname);
157
161
  }
158
162
  });
@@ -1,11 +1,16 @@
1
- import { Knex } from "knex";
2
- import { ORM } from "./orm.js";
3
1
  import { CachedQuery } from "@ghom/query";
2
+ import type { Knex } from "knex";
3
+ import { type ColumnDef, type InferColumns } from "./column.js";
4
+ import type { ORM } from "./orm.js";
4
5
  export interface MigrationData {
5
6
  table: string;
6
7
  version: number;
7
8
  }
8
- export interface TableOptions<Type extends object = object> {
9
+ /**
10
+ * Table options with typed columns.
11
+ * Type is automatically inferred from the column definitions.
12
+ */
13
+ export interface TableOptions<Columns extends Record<string, ColumnDef<any, any>>> {
9
14
  name: string;
10
15
  description?: string;
11
16
  priority?: number;
@@ -15,23 +20,48 @@ export interface TableOptions<Type extends object = object> {
15
20
  */
16
21
  caching?: number;
17
22
  migrations?: {
18
- [version: number]: (table: Knex.CreateTableBuilder) => void;
23
+ [version: number]: (builder: Knex.CreateTableBuilder) => void;
19
24
  };
20
- then?: (this: Table<Type>, table: Table<Type>) => unknown;
21
- setup: (table: Knex.CreateTableBuilder) => void;
25
+ then?: (this: Table<Columns>, table: Table<Columns>) => unknown;
26
+ /**
27
+ * Typed columns definition with automatic type inference.
28
+ *
29
+ * @example
30
+ * columns: (col) => ({
31
+ * id: col.increments(),
32
+ * username: col.string().unique(),
33
+ * age: col.integer().nullable(),
34
+ * role: col.enum(["admin", "user"]),
35
+ * })
36
+ */
37
+ columns: (col: typeof import("./column.js").col) => Columns;
22
38
  }
23
- export declare class Table<Type extends object = object> {
24
- readonly options: TableOptions<Type>;
39
+ /**
40
+ * Represents a database table with typed columns.
41
+ *
42
+ * @example
43
+ * const userTable = new Table({
44
+ * name: "user",
45
+ * columns: (col) => ({
46
+ * id: col.increments(),
47
+ * username: col.string().unique(),
48
+ * age: col.integer().nullable(),
49
+ * }),
50
+ * })
51
+ * // Type is automatically inferred as { id: number; username: string; age: number | null }
52
+ */
53
+ export declare class Table<Columns extends Record<string, ColumnDef<any, any>> = Record<string, ColumnDef<any, any>>> {
54
+ readonly options: TableOptions<Columns>;
25
55
  orm?: ORM;
26
- _whereCache?: CachedQuery<[
27
- cb: (query: Table<Type>["query"]) => unknown
28
- ], unknown>;
56
+ _whereCache?: CachedQuery<[cb: (query: Table<Columns>["query"]) => unknown], unknown>;
29
57
  _countCache?: CachedQuery<[where: string | null], number>;
30
- constructor(options: TableOptions<Type>);
58
+ constructor(options: TableOptions<Columns>);
59
+ /** The inferred TypeScript type for rows of this table */
60
+ readonly $type: InferColumns<Columns>;
31
61
  private requireOrm;
32
- get db(): Knex;
33
- get query(): Knex.QueryBuilder<Type, {
34
- _base: Type;
62
+ get client(): Knex;
63
+ get query(): Knex.QueryBuilder<InferColumns<Columns>, {
64
+ _base: InferColumns<Columns>;
35
65
  _hasSelection: false;
36
66
  _keys: never;
37
67
  _aliases: {};
@@ -40,16 +70,16 @@ export declare class Table<Type extends object = object> {
40
70
  _unionProps: never;
41
71
  }[]>;
42
72
  get cache(): {
43
- get: <Return>(id: string, cb: (table: Pick<Table<Type>["query"], "select" | "count" | "avg" | "sum" | "countDistinct" | "avgDistinct" | "sumDistinct">) => Return) => Return;
44
- set: <Return>(cb: (table: Pick<Table<Type>["query"], "update" | "delete" | "insert" | "upsert" | "truncate" | "jsonInsert">) => Return) => Return;
73
+ get: <Return>(id: string, cb: (table: Pick<Table<Columns>["query"], "select" | "count" | "avg" | "sum" | "countDistinct" | "avgDistinct" | "sumDistinct">) => Return) => Return;
74
+ set: <Return>(cb: (table: Pick<Table<Columns>["query"], "update" | "delete" | "insert" | "upsert" | "truncate" | "jsonInsert">) => Return) => Return;
45
75
  count: (where?: string) => Promise<number>;
46
76
  invalidate: () => void;
47
77
  };
48
78
  count(where?: string): Promise<number>;
49
- hasColumn(name: keyof Type & string): Promise<boolean>;
50
- getColumn(name: keyof Type & string): Promise<Knex.ColumnInfo>;
51
- getColumns(): Promise<Record<keyof Type & string, Knex.ColumnInfo>>;
52
- getColumnNames(): Promise<Array<keyof Type & string>>;
79
+ hasColumn(name: keyof InferColumns<Columns> & string): Promise<boolean>;
80
+ getColumn(name: keyof InferColumns<Columns> & string): Promise<Knex.ColumnInfo>;
81
+ getColumns(): Promise<Record<keyof InferColumns<Columns> & string, Knex.ColumnInfo>>;
82
+ getColumnNames(): Promise<Array<keyof InferColumns<Columns> & string>>;
53
83
  isEmpty(): Promise<boolean>;
54
84
  make(orm: ORM): Promise<this>;
55
85
  private migrate;
package/dist/app/table.js CHANGED
@@ -1,5 +1,20 @@
1
- import { styled } from "./util.js";
2
1
  import { CachedQuery } from "@ghom/query";
2
+ import { buildColumnsSchema, col } from "./column.js";
3
+ import { styled } from "./util.js";
4
+ /**
5
+ * Represents a database table with typed columns.
6
+ *
7
+ * @example
8
+ * const userTable = new Table({
9
+ * name: "user",
10
+ * columns: (col) => ({
11
+ * id: col.increments(),
12
+ * username: col.string().unique(),
13
+ * age: col.integer().nullable(),
14
+ * }),
15
+ * })
16
+ * // Type is automatically inferred as { id: number; username: string; age: number | null }
17
+ */
3
18
  export class Table {
4
19
  options;
5
20
  orm;
@@ -11,15 +26,15 @@ export class Table {
11
26
  requireOrm() {
12
27
  if (!this.orm)
13
28
  throw new Error("missing ORM");
14
- if (!this.orm.client)
29
+ if (!this.orm._client)
15
30
  throw new Error("ORM client is not initialized");
16
31
  }
17
- get db() {
32
+ get client() {
18
33
  this.requireOrm();
19
34
  return this.orm.client;
20
35
  }
21
36
  get query() {
22
- return this.db(this.options.name);
37
+ return this.client(this.options.name);
23
38
  }
24
39
  get cache() {
25
40
  if (!this._whereCache || !this._countCache)
@@ -46,18 +61,18 @@ export class Table {
46
61
  }
47
62
  async count(where) {
48
63
  return this.query
49
- .select(this.db.raw("count(*) as total"))
64
+ .select(this.client.raw("count(*) as total"))
50
65
  .whereRaw(where ?? "1=1")
51
66
  .then((rows) => +(rows?.[0] ?? { total: 0 }).total);
52
67
  }
53
68
  async hasColumn(name) {
54
- return this.db.schema.hasColumn(this.options.name, name);
69
+ return this.client.schema.hasColumn(this.options.name, name);
55
70
  }
56
71
  async getColumn(name) {
57
- return this.db(this.options.name).columnInfo(name);
72
+ return this.client(this.options.name).columnInfo(name);
58
73
  }
59
74
  async getColumns() {
60
- return this.db(this.options.name).columnInfo();
75
+ return this.client(this.options.name).columnInfo();
61
76
  }
62
77
  async getColumnNames() {
63
78
  return this.getColumns().then(Object.keys);
@@ -74,12 +89,15 @@ export class Table {
74
89
  ? ` ${styled(this.orm, this.options.description, "description")}`
75
90
  : ""}`;
76
91
  try {
77
- await this.db.schema.createTable(this.options.name, this.options.setup);
92
+ await this.client.schema.createTable(this.options.name, (builder) => {
93
+ const columns = this.options.columns(col);
94
+ buildColumnsSchema(builder, columns);
95
+ });
78
96
  this.orm.config.logger?.log(`created table ${tableNameLog}`);
79
97
  }
80
98
  catch (error) {
81
99
  if (error.toString().includes("syntax error")) {
82
- this.orm.config.logger?.error(`you need to implement the "setup" method in options of your ${styled(this.orm, this.options.name, "highlight")} table!`);
100
+ this.orm.config.logger?.error(`you need to implement the "columns" callback in options of your ${styled(this.orm, this.options.name, "highlight")} table!`);
83
101
  throw error;
84
102
  }
85
103
  else {
@@ -96,8 +114,10 @@ export class Table {
96
114
  this.orm.config.logger?.error(error);
97
115
  throw error;
98
116
  }
99
- if ((await this.count()) === 0)
100
- await this.options.then?.bind(this)(this);
117
+ if ((await this.count()) === 0) {
118
+ const thenFn = this.options.then;
119
+ await thenFn?.bind(this)(this);
120
+ }
101
121
  return this;
102
122
  }
103
123
  async migrate() {
@@ -106,7 +126,7 @@ export class Table {
106
126
  const migrations = new Map(Object.entries(this.options.migrations)
107
127
  .sort((a, b) => Number(a[0]) - Number(b[0]))
108
128
  .map((entry) => [Number(entry[0]), entry[1]]));
109
- const fromDatabase = await this.db("migration")
129
+ const fromDatabase = await this.client("migration")
110
130
  .where("table", this.options.name)
111
131
  .first();
112
132
  const data = fromDatabase || {
@@ -115,17 +135,14 @@ export class Table {
115
135
  };
116
136
  const baseVersion = data.version;
117
137
  for (const [version, migration] of migrations) {
118
- await this.db.schema.alterTable(this.options.name, (builder) => {
138
+ await this.client.schema.alterTable(this.options.name, (builder) => {
119
139
  if (version <= data.version)
120
140
  return;
121
141
  migration(builder);
122
142
  data.version = version;
123
143
  });
124
144
  }
125
- await this.db("migration")
126
- .insert(data)
127
- .onConflict("table")
128
- .merge();
145
+ await this.client("migration").insert(data).onConflict("table").merge();
129
146
  return baseVersion === data.version ? false : data.version;
130
147
  }
131
148
  }
@@ -4,7 +4,7 @@ export type TextStyle = Parameters<typeof util.styleText>[0];
4
4
  export declare const DEFAULT_BACKUP_LOCATION: string;
5
5
  export declare const DEFAULT_BACKUP_CHUNK_SIZE: number;
6
6
  export declare const DEFAULT_LOGGER_HIGHLIGHT = "blueBright";
7
- export declare const DEFAULT_LOGGER_DESCRIPTION = "grey";
7
+ export declare const DEFAULT_LOGGER_DESCRIPTION = "gray";
8
8
  export declare const DEFAULT_LOGGER_RAW_VALUE = "magentaBright";
9
9
  declare let isCJS: boolean;
10
10
  export { isCJS };
package/dist/app/util.js CHANGED
@@ -1,15 +1,15 @@
1
- import util from "node:util";
2
- import path from "node:path";
3
1
  import fs from "node:fs";
2
+ import path from "node:path";
3
+ import util from "node:util";
4
4
  export const DEFAULT_BACKUP_LOCATION = path.join(process.cwd(), "backup");
5
5
  export const DEFAULT_BACKUP_CHUNK_SIZE = 5 * 1024 * 1024; // 5MB
6
6
  export const DEFAULT_LOGGER_HIGHLIGHT = "blueBright";
7
- export const DEFAULT_LOGGER_DESCRIPTION = "grey";
7
+ export const DEFAULT_LOGGER_DESCRIPTION = "gray";
8
8
  export const DEFAULT_LOGGER_RAW_VALUE = "magentaBright";
9
9
  let isCJS = false;
10
10
  try {
11
11
  const pack = JSON.parse(fs.readFileSync(path.join(process.cwd(), "package.json"), "utf8"));
12
- isCJS = pack.type === "commonjs" || pack.type == void 0;
12
+ isCJS = pack.type === "commonjs" || pack.type === void 0;
13
13
  }
14
14
  catch {
15
15
  throw new Error("Missing package.json: Can't detect the type of modules.\n" +
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
+ export * from "./app/backup.js";
2
+ export * from "./app/column.js";
1
3
  export * from "./app/orm.js";
2
4
  export * from "./app/table.js";
3
- export * from "./app/backup.js";
4
5
  export * from "./app/util.js";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
+ export * from "./app/backup.js";
2
+ export * from "./app/column.js";
1
3
  export * from "./app/orm.js";
2
4
  export * from "./app/table.js";
3
- export * from "./app/backup.js";
4
5
  export * from "./app/util.js";
package/package.json CHANGED
@@ -1,47 +1,43 @@
1
- {
2
- "name": "@ghom/orm",
3
- "version": "1.10.0",
4
- "license": "MIT",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "description": "TypeScript KnexJS ORM & handler",
9
- "homepage": "https://github.com/GhomKrosmonaute/orm",
10
- "prettier": {
11
- "semi": false
12
- },
13
- "scripts": {
14
- "format": "prettier --write src tsconfig.json tests",
15
- "build": "rimraf dist && tsc",
16
- "test": "npm run build && node --experimental-vm-modules node_modules/jest/bin/jest.js tests/test.js --detectOpenHandles",
17
- "prepublishOnly": "npm run format && npm test"
18
- },
19
- "devDependencies": {
20
- "@types/jest": "^29.5.6",
21
- "@types/node": "^22.0.0",
22
- "dotenv": "^16.3.1",
23
- "jest": "^29.7.0",
24
- "prettier": "^3.0.3",
25
- "rimraf": "^6.0.1",
26
- "typescript": "^5.2.2"
27
- },
28
- "optionalDependencies": {
29
- "mysql2": "^3.6.2",
30
- "pg": "^8.11.3",
31
- "sqlite3": "^5.1.6"
32
- },
33
- "dependencies": {
34
- "@ghom/handler": "^3.1.0",
35
- "@ghom/query": "1.0.0",
36
- "csv-parser": "^3.0.0",
37
- "json-2-csv": "^5.5.6",
38
- "knex": "^3.0.1"
39
- },
40
- "engines": {
41
- "node": ">=22.0.0"
42
- },
43
- "repository": {
44
- "url": "https://github.com/GhomKrosmonaute/orm.git",
45
- "type": "git"
46
- }
47
- }
1
+ {
2
+ "name": "@ghom/orm",
3
+ "version": "2.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "description": "TypeScript KnexJS ORM & handler",
9
+ "homepage": "https://github.com/GhomKrosmonaute/orm",
10
+ "scripts": {
11
+ "format": "biome format --write src",
12
+ "lint": "biome lint .",
13
+ "check": "biome check --write .",
14
+ "build": "rimraf dist && tsc",
15
+ "test": "bun test",
16
+ "prepublishOnly": "npm run check && npm run build && npm test"
17
+ },
18
+ "devDependencies": {
19
+ "@biomejs/biome": "^2.3.13",
20
+ "@types/bun": "^1.1.0",
21
+ "rimraf": "^6.0.1",
22
+ "typescript": "^5.2.2"
23
+ },
24
+ "optionalDependencies": {
25
+ "mysql2": "^3.6.2",
26
+ "pg": "^8.11.3",
27
+ "sqlite3": "^5.1.6"
28
+ },
29
+ "dependencies": {
30
+ "@ghom/handler": "^3.1.0",
31
+ "@ghom/query": "1.0.0",
32
+ "csv-parser": "^3.0.0",
33
+ "json-2-csv": "^5.5.6",
34
+ "knex": "^3.0.1"
35
+ },
36
+ "engines": {
37
+ "node": ">=22.0.0"
38
+ },
39
+ "repository": {
40
+ "url": "https://github.com/GhomKrosmonaute/orm.git",
41
+ "type": "git"
42
+ }
43
+ }