@ghom/orm 1.9.1 → 1.10.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.
@@ -3,13 +3,19 @@ import path from "node:path";
3
3
  import csv from "json-2-csv";
4
4
  import csvParser from "csv-parser";
5
5
  import { DEFAULT_BACKUP_CHUNK_SIZE, DEFAULT_BACKUP_LOCATION, styled, } from "./util.js";
6
+ function getConfig(orm) {
7
+ if (orm.config === false)
8
+ throw new Error("ORM config is not available");
9
+ return orm.config;
10
+ }
6
11
  export async function backupTable(table, dirname) {
7
12
  if (!table.orm)
8
13
  throw new Error("missing ORM");
14
+ const config = getConfig(table.orm);
9
15
  let offset = 0;
10
16
  let processedRows = 0;
11
17
  let chunkIndex = 0;
12
- const chunkDir = path.join(table.orm.config.backups?.location ?? DEFAULT_BACKUP_LOCATION, dirname ?? "");
18
+ const chunkDir = path.join(config.backups?.location ?? DEFAULT_BACKUP_LOCATION, dirname ?? "");
13
19
  if (!fs.existsSync(chunkDir)) {
14
20
  fs.mkdirSync(chunkDir, { recursive: true });
15
21
  console.log(`Backup directory ${styled(table.orm, path.relative(process.cwd(), chunkDir), "highlight")} created.`);
@@ -28,7 +34,7 @@ export async function backupTable(table, dirname) {
28
34
  // Si aucun fichier n'est créé ou qu'on a dépassé la taille max du chunk, on crée un nouveau fichier CSV
29
35
  if (!writeStream ||
30
36
  writeStream.bytesWritten + Buffer.byteLength(csvData, "utf8") >
31
- (table.orm.config.backups?.chunkSize ?? DEFAULT_BACKUP_CHUNK_SIZE)) {
37
+ (config.backups?.chunkSize ?? DEFAULT_BACKUP_CHUNK_SIZE)) {
32
38
  if (writeStream) {
33
39
  closePromises.push(new Promise((resolve) => writeStream.end(resolve))); // Ajouter la promesse de fermeture
34
40
  }
@@ -58,7 +64,8 @@ export async function backupTable(table, dirname) {
58
64
  export async function restoreBackup(table, trx, dirname) {
59
65
  if (!table.orm)
60
66
  throw new Error("missing ORM");
61
- const chunkDir = path.join(table.orm.config.backups?.location ?? DEFAULT_BACKUP_LOCATION, dirname ?? "");
67
+ const config = getConfig(table.orm);
68
+ const chunkDir = path.join(config.backups?.location ?? DEFAULT_BACKUP_LOCATION, dirname ?? "");
62
69
  const chunkFiles = fs
63
70
  .readdirSync(chunkDir)
64
71
  .filter((file) => file.split("_chunk_")[0] === table.options.name);
@@ -112,6 +119,8 @@ export async function enableForeignKeys(orm, trx) {
112
119
  });
113
120
  }
114
121
  export async function disableForeignKeys(orm, run) {
122
+ if (!orm.client)
123
+ throw new Error("ORM client is not initialized");
115
124
  const trx = orm.clientBasedOperation({
116
125
  sqlite3: () => orm.client,
117
126
  }) ?? (await orm.client.transaction());
package/dist/app/orm.d.ts CHANGED
@@ -2,7 +2,7 @@ import { Handler } from "@ghom/handler";
2
2
  import { Knex } from "knex";
3
3
  import { TextStyle } from "./util.js";
4
4
  import { Table } from "./table.js";
5
- import { ResponseCache } from "./caching.js";
5
+ import { CachedQuery } from "@ghom/query";
6
6
  export interface ILogger {
7
7
  log: (message: string) => void;
8
8
  error: (error: string | Error) => void;
@@ -44,13 +44,46 @@ export interface ORMConfig {
44
44
  */
45
45
  caching?: number;
46
46
  }
47
+ /**
48
+ * The main ORM class that manages database connections, tables, and caching.
49
+ *
50
+ * @example
51
+ * // With database connection
52
+ * const orm = new ORM({
53
+ * tableLocation: "./tables",
54
+ * database: { client: "sqlite3", connection: { filename: ":memory:" } }
55
+ * })
56
+ * await orm.init()
57
+ *
58
+ * @example
59
+ * // Without database connection (for type exports only)
60
+ * const orm = new ORM(false)
61
+ * orm.isConnected // false
62
+ */
47
63
  export declare class ORM {
48
- config: ORMConfig;
64
+ config: ORMConfig | false;
49
65
  private _ready;
50
- client: Knex<any, unknown[]>;
51
- handler: Handler<Table<any>>;
52
- _rawCache: ResponseCache<[raw: string], Knex.Raw<any>>;
53
- constructor(config: ORMConfig);
66
+ client?: Knex;
67
+ handler?: Handler<Table<any>>;
68
+ _rawCache?: CachedQuery<[raw: string], Knex.Raw>;
69
+ /**
70
+ * Creates a new ORM instance.
71
+ *
72
+ * @param config - The ORM configuration, or `false` to create an unconnected instance.
73
+ *
74
+ * When `false` is passed, the ORM will not connect to any database.
75
+ * This is useful for scenarios where you only need to export types or
76
+ * use the ORM structure without an actual database connection.
77
+ *
78
+ * Methods that require a database connection will throw an error
79
+ * if called on an unconnected ORM instance.
80
+ */
81
+ constructor(config: ORMConfig | false);
82
+ private requireClient;
83
+ /**
84
+ * Returns true if the ORM has a database client connected.
85
+ */
86
+ get isConnected(): boolean;
54
87
  get cachedTables(): Table<any>[];
55
88
  get cachedTableNames(): string[];
56
89
  hasCachedTable(name: string): boolean;
@@ -61,7 +94,7 @@ export declare class ORM {
61
94
  init(): Promise<void>;
62
95
  raw(sql: Knex.Value): Knex.Raw;
63
96
  cache: {
64
- raw: (sql: string, anyDataUpdated?: boolean) => Knex.Raw;
97
+ raw: (sql: string, anyDataUpdated?: boolean) => Promise<Knex.Raw>;
65
98
  invalidate: () => void;
66
99
  };
67
100
  clientBasedOperation<Return>(operation: Partial<Record<"pg" | "mysql2" | "sqlite3", () => Return>>): Return | undefined;
package/dist/app/orm.js CHANGED
@@ -3,16 +3,46 @@ import { Handler } from "@ghom/handler";
3
3
  import { default as knex } from "knex";
4
4
  import { isCJS } from "./util.js";
5
5
  import { Table } from "./table.js";
6
- import { ResponseCache } from "./caching.js";
6
+ import { CachedQuery } from "@ghom/query";
7
7
  import { backupTable, restoreBackup, disableForeignKeys, enableForeignKeys, } from "./backup.js";
8
+ /**
9
+ * The main ORM class that manages database connections, tables, and caching.
10
+ *
11
+ * @example
12
+ * // With database connection
13
+ * const orm = new ORM({
14
+ * tableLocation: "./tables",
15
+ * database: { client: "sqlite3", connection: { filename: ":memory:" } }
16
+ * })
17
+ * await orm.init()
18
+ *
19
+ * @example
20
+ * // Without database connection (for type exports only)
21
+ * const orm = new ORM(false)
22
+ * orm.isConnected // false
23
+ */
8
24
  export class ORM {
9
25
  config;
10
26
  _ready = false;
11
27
  client;
12
28
  handler;
13
29
  _rawCache;
30
+ /**
31
+ * Creates a new ORM instance.
32
+ *
33
+ * @param config - The ORM configuration, or `false` to create an unconnected instance.
34
+ *
35
+ * When `false` is passed, the ORM will not connect to any database.
36
+ * This is useful for scenarios where you only need to export types or
37
+ * use the ORM structure without an actual database connection.
38
+ *
39
+ * Methods that require a database connection will throw an error
40
+ * if called on an unconnected ORM instance.
41
+ */
14
42
  constructor(config) {
15
43
  this.config = config;
44
+ if (config === false)
45
+ return;
16
46
  this.client = knex(config.database ?? {
17
47
  client: "sqlite3",
18
48
  useNullAsDefault: true,
@@ -29,9 +59,21 @@ export class ORM {
29
59
  throw new Error(`${filepath}: default export must be a Table instance`);
30
60
  },
31
61
  });
32
- this._rawCache = new ResponseCache((raw) => this.raw(raw), config.caching ?? Infinity);
62
+ this._rawCache = new CachedQuery(async (raw) => await this.raw(raw), config.caching ?? Infinity);
63
+ }
64
+ requireClient() {
65
+ if (!this.client)
66
+ throw new Error("ORM client is not initialized. Cannot use this method without a database connection.");
67
+ }
68
+ /**
69
+ * Returns true if the ORM has a database client connected.
70
+ */
71
+ get isConnected() {
72
+ return this.client !== undefined;
33
73
  }
34
74
  get cachedTables() {
75
+ if (!this.handler)
76
+ return [];
35
77
  return [...this.handler.elements.values()];
36
78
  }
37
79
  get cachedTableNames() {
@@ -41,12 +83,14 @@ export class ORM {
41
83
  return this.cachedTables.some((table) => table.options.name === name);
42
84
  }
43
85
  async hasTable(name) {
86
+ this.requireClient();
44
87
  return this.client.schema.hasTable(name);
45
88
  }
46
89
  /**
47
90
  * Handle the table files and create the tables in the database.
48
91
  */
49
92
  async init() {
93
+ this.requireClient();
50
94
  await this.handler.init();
51
95
  try {
52
96
  await enableForeignKeys(this);
@@ -67,22 +111,26 @@ export class ORM {
67
111
  this._ready = true;
68
112
  }
69
113
  raw(sql) {
114
+ this.requireClient();
70
115
  if (this._ready)
71
116
  this.cache.invalidate();
72
117
  return this.client.raw(sql);
73
118
  }
74
119
  cache = {
75
120
  raw: (sql, anyDataUpdated) => {
121
+ this.requireClient();
76
122
  if (anyDataUpdated)
77
123
  this.cache.invalidate();
78
124
  return this._rawCache.get(sql, sql);
79
125
  },
80
126
  invalidate: () => {
81
- this._rawCache.invalidate();
127
+ this._rawCache?.invalidate();
82
128
  this.cachedTables.forEach((table) => table.cache.invalidate());
83
129
  },
84
130
  };
85
131
  clientBasedOperation(operation) {
132
+ if (this.config === false)
133
+ return undefined;
86
134
  const client = (this.config.database?.client ?? "sqlite3");
87
135
  return operation[client]?.();
88
136
  }
@@ -91,6 +139,7 @@ export class ORM {
91
139
  * The backup will be saved in the location specified in the config.
92
140
  */
93
141
  async createBackup(dirname) {
142
+ this.requireClient();
94
143
  for (let table of this.cachedTables) {
95
144
  await backupTable(table, dirname);
96
145
  }
@@ -101,6 +150,7 @@ export class ORM {
101
150
  * @warning This will delete all the data in the tables.
102
151
  */
103
152
  async restoreBackup(dirname) {
153
+ this.requireClient();
104
154
  await disableForeignKeys(this, async (trx) => {
105
155
  for (let table of this.cachedTables) {
106
156
  await restoreBackup(table, trx, dirname);
@@ -1,6 +1,6 @@
1
1
  import { Knex } from "knex";
2
2
  import { ORM } from "./orm.js";
3
- import { ResponseCache } from "./caching.js";
3
+ import { CachedQuery } from "@ghom/query";
4
4
  export interface MigrationData {
5
5
  table: string;
6
6
  version: number;
@@ -23,12 +23,13 @@ export interface TableOptions<Type extends object = object> {
23
23
  export declare class Table<Type extends object = object> {
24
24
  readonly options: TableOptions<Type>;
25
25
  orm?: ORM;
26
- _whereCache?: ResponseCache<[
26
+ _whereCache?: CachedQuery<[
27
27
  cb: (query: Table<Type>["query"]) => unknown
28
28
  ], unknown>;
29
- _countCache?: ResponseCache<[where: string | null], Promise<number>>;
29
+ _countCache?: CachedQuery<[where: string | null], number>;
30
30
  constructor(options: TableOptions<Type>);
31
- get db(): Knex<any, unknown[]>;
31
+ private requireOrm;
32
+ get db(): Knex;
32
33
  get query(): Knex.QueryBuilder<Type, {
33
34
  _base: Type;
34
35
  _hasSelection: false;
package/dist/app/table.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { styled } from "./util.js";
2
- import { ResponseCache } from "./caching.js";
2
+ import { CachedQuery } from "@ghom/query";
3
3
  export class Table {
4
4
  options;
5
5
  orm;
@@ -8,9 +8,14 @@ export class Table {
8
8
  constructor(options) {
9
9
  this.options = options;
10
10
  }
11
- get db() {
11
+ requireOrm() {
12
12
  if (!this.orm)
13
13
  throw new Error("missing ORM");
14
+ if (!this.orm.client)
15
+ throw new Error("ORM client is not initialized");
16
+ }
17
+ get db() {
18
+ this.requireOrm();
14
19
  return this.orm.client;
15
20
  }
16
21
  get query() {
@@ -19,8 +24,7 @@ export class Table {
19
24
  get cache() {
20
25
  if (!this._whereCache || !this._countCache)
21
26
  throw new Error("missing cache");
22
- if (!this.orm)
23
- throw new Error("missing ORM");
27
+ this.requireOrm();
24
28
  return {
25
29
  get: (id, cb) => {
26
30
  return this._whereCache.get(id, cb);
@@ -63,8 +67,9 @@ export class Table {
63
67
  }
64
68
  async make(orm) {
65
69
  this.orm = orm;
66
- this._whereCache = new ResponseCache((cb) => cb(this.query), this.options.caching ?? this.orm?.config.caching ?? Infinity);
67
- this._countCache = new ResponseCache((where) => this.count(where ?? undefined), this.options.caching ?? this.orm?.config.caching ?? Infinity);
70
+ this.requireOrm();
71
+ this._whereCache = new CachedQuery((cb) => cb(this.query), this.options.caching ?? this.orm.config.caching ?? Infinity);
72
+ this._countCache = new CachedQuery((where) => this.count(where ?? undefined), this.options.caching ?? this.orm.config.caching ?? Infinity);
68
73
  const tableNameLog = `table ${styled(this.orm, this.options.name, "highlight")}${this.options.description
69
74
  ? ` ${styled(this.orm, this.options.description, "description")}`
70
75
  : ""}`;
package/dist/app/util.js CHANGED
@@ -18,7 +18,8 @@ catch {
18
18
  }
19
19
  export { isCJS };
20
20
  export function styled(orm, message, style) {
21
- return util.styleText(orm.config.loggerStyles?.[style] ??
21
+ const config = orm.config !== false ? orm.config : undefined;
22
+ return util.styleText(config?.loggerStyles?.[style] ??
22
23
  (style === "highlight"
23
24
  ? DEFAULT_LOGGER_HIGHLIGHT
24
25
  : style === "rawValue"
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export * from "./app/orm.js";
2
2
  export * from "./app/table.js";
3
- export * from "./app/caching.js";
4
3
  export * from "./app/backup.js";
5
4
  export * from "./app/util.js";
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  export * from "./app/orm.js";
2
2
  export * from "./app/table.js";
3
- export * from "./app/caching.js";
4
3
  export * from "./app/backup.js";
5
4
  export * from "./app/util.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghom/orm",
3
- "version": "1.9.1",
3
+ "version": "1.10.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@ghom/handler": "^3.1.0",
35
+ "@ghom/query": "1.0.0",
35
36
  "csv-parser": "^3.0.0",
36
37
  "json-2-csv": "^5.5.6",
37
38
  "knex": "^3.0.1"
package/readme.md CHANGED
@@ -41,6 +41,25 @@ const orm = new ORM({
41
41
  await orm.init()
42
42
  ```
43
43
 
44
+ ### Unconnected ORM
45
+
46
+ You can also create an ORM instance without connecting to a database. This is useful when you only need to export types or prepare the ORM structure for a future database connection.
47
+
48
+ ```typescript
49
+ import { ORM } from "@ghom/orm"
50
+
51
+ const orm = new ORM(false)
52
+
53
+ orm.isConnected // false
54
+ orm.cachedTables // []
55
+ orm.cachedTableNames // []
56
+
57
+ // Methods requiring a database connection will throw an error
58
+ orm.init() // throws Error
59
+ orm.hasTable("user") // throws Error
60
+ orm.raw("SELECT 1") // throws Error
61
+ ```
62
+
44
63
  ## Add tables
45
64
 
46
65
  The tables are automatically loaded from the `location` directory.
package/tests/test.js CHANGED
@@ -16,6 +16,30 @@ import a from "./tables/a"
16
16
  import b from "./tables/b"
17
17
  import c from "./tables/c"
18
18
 
19
+ describe("unconnected ORM", () => {
20
+ test("can be initialized with false", () => {
21
+ const unconnectedOrm = new ORM(false)
22
+
23
+ expect(unconnectedOrm).toBeInstanceOf(ORM)
24
+ expect(unconnectedOrm.isConnected).toBe(false)
25
+ expect(unconnectedOrm.config).toBe(false)
26
+ expect(unconnectedOrm.client).toBeUndefined()
27
+ expect(unconnectedOrm.handler).toBeUndefined()
28
+ expect(unconnectedOrm.cachedTables).toEqual([])
29
+ expect(unconnectedOrm.cachedTableNames).toEqual([])
30
+ })
31
+
32
+ test("throws when calling methods requiring client", async () => {
33
+ const unconnectedOrm = new ORM(false)
34
+
35
+ await expect(unconnectedOrm.init()).rejects.toThrow()
36
+ await expect(unconnectedOrm.hasTable("test")).rejects.toThrow()
37
+ expect(() => unconnectedOrm.raw("SELECT 1")).toThrow()
38
+ await expect(unconnectedOrm.createBackup()).rejects.toThrow()
39
+ await expect(unconnectedOrm.restoreBackup()).rejects.toThrow()
40
+ })
41
+ })
42
+
19
43
  const orm = new ORM({
20
44
  tableLocation: path.join(process.cwd(), "tests", "tables"),
21
45
  backups: {
@@ -1,18 +0,0 @@
1
- export interface ResponseCacheData<Value> {
2
- value: Value;
3
- expires: number;
4
- outdated?: boolean;
5
- }
6
- /**
7
- * Advanced cache for async queries
8
- */
9
- export declare class ResponseCache<Params extends any[], Value> {
10
- private _request;
11
- private _timeout;
12
- private _cache;
13
- constructor(_request: (...params: Params) => Value, _timeout: number);
14
- get(id: string, ...params: Params): Value;
15
- fetch(id: string, ...params: Params): Value;
16
- invalidate(): void;
17
- invalidate(id: string): void;
18
- }
@@ -1,36 +0,0 @@
1
- /**
2
- * Advanced cache for async queries
3
- */
4
- export class ResponseCache {
5
- _request;
6
- _timeout;
7
- _cache = new Map();
8
- constructor(_request, _timeout) {
9
- this._request = _request;
10
- this._timeout = _timeout;
11
- }
12
- get(id, ...params) {
13
- const cached = this._cache.get(id);
14
- if (!cached || cached.expires < Date.now()) {
15
- this._cache.set(id, {
16
- value: this._request(...params),
17
- expires: Date.now() + this._timeout,
18
- });
19
- }
20
- return this._cache.get(id).value;
21
- }
22
- fetch(id, ...params) {
23
- this._cache.set(id, {
24
- value: this._request(...params),
25
- expires: Date.now() + this._timeout,
26
- });
27
- return this._cache.get(id).value;
28
- }
29
- invalidate(id) {
30
- if (!id) {
31
- this._cache.clear();
32
- return;
33
- }
34
- this._cache.delete(id);
35
- }
36
- }