@ghom/orm 1.7.1 → 1.8.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/src/app/table.ts CHANGED
@@ -1,6 +1,12 @@
1
- import chalk from "chalk"
1
+ import util from "util"
2
2
  import { Knex } from "knex"
3
3
  import { ORM } from "./orm.js"
4
+ import { ResponseCache } from "./caching.js"
5
+ import {
6
+ DEFAULT_LOGGER_DESCRIPTION,
7
+ DEFAULT_LOGGER_HIGHLIGHT,
8
+ DEFAULT_LOGGER_RAW_VALUE,
9
+ } from "./util.js"
4
10
 
5
11
  export interface MigrationData {
6
12
  table: string
@@ -11,13 +17,25 @@ export interface TableOptions<Type extends object = object> {
11
17
  name: string
12
18
  description?: string
13
19
  priority?: number
20
+ /**
21
+ * The cache time in milliseconds. <br>
22
+ * Default is `Infinity`.
23
+ */
24
+ caching?: number
14
25
  migrations?: { [version: number]: (table: Knex.CreateTableBuilder) => void }
15
26
  then?: (this: Table<Type>, table: Table<Type>) => unknown
16
27
  setup: (table: Knex.CreateTableBuilder) => void
17
28
  }
18
29
 
19
30
  export class Table<Type extends object = object> {
20
- orm?: ORM
31
+ public orm?: ORM
32
+
33
+ public _whereCache?: ResponseCache<
34
+ [cb: (query: Table<Type>["query"]) => unknown],
35
+ unknown
36
+ >
37
+
38
+ public _countCache?: ResponseCache<[where: string | null], Promise<number>>
21
39
 
22
40
  constructor(public readonly options: TableOptions<Type>) {}
23
41
 
@@ -30,6 +48,57 @@ export class Table<Type extends object = object> {
30
48
  return this.db<Type>(this.options.name)
31
49
  }
32
50
 
51
+ get cache() {
52
+ if (!this._whereCache || !this._countCache) throw new Error("missing cache")
53
+
54
+ if (!this.orm) throw new Error("missing ORM")
55
+
56
+ return {
57
+ get: <Return>(
58
+ id: string,
59
+ cb: (
60
+ table: Pick<
61
+ Table<Type>["query"],
62
+ | "select"
63
+ | "count"
64
+ | "avg"
65
+ | "sum"
66
+ | "countDistinct"
67
+ | "avgDistinct"
68
+ | "sumDistinct"
69
+ >,
70
+ ) => Return,
71
+ ): Return => {
72
+ return this._whereCache!.get(id, cb) as Return
73
+ },
74
+ set: <Return>(
75
+ cb: (
76
+ table: Pick<
77
+ Table<Type>["query"],
78
+ | "update"
79
+ | "delete"
80
+ | "insert"
81
+ | "upsert"
82
+ | "truncate"
83
+ | "jsonInsert"
84
+ >,
85
+ ) => Return,
86
+ ) => {
87
+ // todo: invalidate only the related tables
88
+ this.orm!.cache.invalidate()
89
+ return cb(this.query)
90
+ },
91
+ count: (where?: string) => {
92
+ return this._countCache!.get(where ?? "*", where ?? null)
93
+ },
94
+ invalidate: () => {
95
+ this._whereCache!.invalidate()
96
+ this._countCache!.invalidate()
97
+ this.orm!._rawCache.invalidate()
98
+ },
99
+ }
100
+ }
101
+
33
102
  async count(where?: string): Promise<number> {
34
103
  return this.query
35
104
  .select(this.db.raw("count(*) as total"))
@@ -56,21 +125,34 @@ export class Table<Type extends object = object> {
56
125
  }
57
126
 
58
127
  async isEmpty(): Promise<boolean> {
59
- return this.count().then((count) => count === 0)
128
+ return this.count().then((count) => +count === 0)
60
129
  }
61
130
 
62
131
  async make(): Promise<this> {
63
132
  if (!this.orm) throw new Error("missing ORM")
64
133
 
134
+ this._whereCache = new ResponseCache(
135
+ (cb: (query: Knex.QueryBuilder<Type>) => unknown) => cb(this.query),
136
+ this.options.caching ?? this.orm?.config.caching ?? Infinity,
137
+ )
138
+
139
+ this._countCache = new ResponseCache(
140
+ (where: string | null) => this.count(where ?? undefined),
141
+ this.options.caching ?? this.orm?.config.caching ?? Infinity,
142
+ )
143
+
65
144
  try {
66
145
  await this.db.schema.createTable(this.options.name, this.options.setup)
67
146
 
68
147
  this.orm.config.logger?.log(
69
- `created table ${chalk[
70
- this.orm.config.loggerColors?.highlight ?? "blueBright"
71
- ](this.options.name)}${
148
+ `created table ${util.styleText(
149
+ this.orm.config.loggerStyles?.highlight ?? DEFAULT_LOGGER_HIGHLIGHT,
150
+ this.options.name,
151
+ )}${
72
152
  this.options.description
73
- ? ` ${chalk[this.orm.config.loggerColors?.description ?? "grey"](
153
+ ? ` ${util.styleText(
154
+ this.orm.config.loggerStyles?.description ??
155
+ DEFAULT_LOGGER_DESCRIPTION,
74
156
  this.options.description,
75
157
  )}`
76
158
  : ""
@@ -79,19 +161,23 @@ export class Table<Type extends object = object> {
79
161
  } catch (error: any) {
80
162
  if (error.toString().includes("syntax error")) {
81
163
  this.orm.config.logger?.error(
82
- `you need to implement the "setup" method in options of your ${chalk[
83
- this.orm.config.loggerColors?.highlight ?? "blueBright"
84
- ](this.options.name)} table!`,
164
+ `you need to implement the "setup" method in options of your ${util.styleText(
165
+ this.orm.config.loggerStyles?.highlight ?? DEFAULT_LOGGER_HIGHLIGHT,
166
+ this.options.name,
167
+ )} table!`,
85
168
  )
86
169
 
87
170
  throw error
88
171
  } else {
89
172
  this.orm.config.logger?.log(
90
- `loaded table ${chalk[
91
- this.orm.config.loggerColors?.highlight ?? "blueBright"
92
- ](this.options.name)}${
173
+ `loaded table ${util.styleText(
174
+ this.orm.config.loggerStyles?.highlight ?? DEFAULT_LOGGER_HIGHLIGHT,
175
+ this.options.name,
176
+ )}${
93
177
  this.options.description
94
- ? ` ${chalk[this.orm.config.loggerColors?.description ?? "grey"](
178
+ ? ` ${util.styleText(
179
+ this.orm.config.loggerStyles?.description ??
180
+ DEFAULT_LOGGER_DESCRIPTION,
95
181
  this.options.description,
96
182
  )}`
97
183
  : ""
@@ -105,11 +191,13 @@ export class Table<Type extends object = object> {
105
191
 
106
192
  if (migrated !== false) {
107
193
  this.orm.config.logger?.log(
108
- `migrated table ${chalk[
109
- this.orm.config.loggerColors?.highlight ?? "blueBright"
110
- ](this.options.name)} to version ${chalk[
111
- this.orm.config.loggerColors?.rawValue ?? "magentaBright"
112
- ](migrated)}`,
194
+ `migrated table ${util.styleText(
195
+ this.orm.config.loggerStyles?.highlight ?? DEFAULT_LOGGER_HIGHLIGHT,
196
+ this.options.name,
197
+ )} to version ${util.styleText(
198
+ this.orm.config.loggerStyles?.rawValue ?? DEFAULT_LOGGER_RAW_VALUE,
199
+ String(migrated),
200
+ )}`,
113
201
  )
114
202
  }
115
203
  } catch (error: any) {
@@ -0,0 +1,30 @@
1
+ import util from "util"
2
+ import path from "path"
3
+ import fs from "fs"
4
+
5
+ export type TextStyle = Parameters<typeof util.styleText>[0]
6
+
7
+ export const DEFAULT_BACKUP_LOCATION = path.join(process.cwd(), "backup")
8
+ export const DEFAULT_BACKUP_CHUNK_SIZE = 5 * 1024 * 1024 // 5MB
9
+
10
+ export const DEFAULT_LOGGER_HIGHLIGHT = "blueBright"
11
+ export const DEFAULT_LOGGER_DESCRIPTION = "grey"
12
+ export const DEFAULT_LOGGER_RAW_VALUE = "magentaBright"
13
+
14
+ let isCJS: boolean = false
15
+
16
+ try {
17
+ const pack = JSON.parse(
18
+ fs.readFileSync(path.join(process.cwd(), "package.json"), "utf8"),
19
+ )
20
+
21
+ isCJS = pack.type === "commonjs" || pack.type == void 0
22
+ } catch {
23
+ throw new Error(
24
+ "Missing package.json: Can't detect the type of modules.\n" +
25
+ "The ORM needs a package.json file present in the process's current working directory.\n" +
26
+ "Please create a package.json file or run the project from another entry point.",
27
+ )
28
+ }
29
+
30
+ export { isCJS }
package/src/index.ts CHANGED
@@ -1,2 +1,5 @@
1
1
  export * from "./app/orm.js"
2
2
  export * from "./app/table.js"
3
+ export * from "./app/caching.js"
4
+ export * from "./app/backup.js"
5
+ export * from "./app/util.js"
package/tests/tables/a.js CHANGED
@@ -1,21 +1,24 @@
1
- import { Table } from "../.."
2
-
3
- export default new Table({
4
- name: "a",
5
- priority: 0,
6
- setup(table) {
7
- table.increments("id").primary().notNullable()
8
- table
9
- .integer("b_id")
10
- .references("id")
11
- .inTable("b")
12
- .onDelete("cascade")
13
- .notNullable()
14
- },
15
- async then({ query }) {
16
- await query.insert({
17
- id: 0,
18
- b_id: 0,
19
- })
20
- },
21
- })
1
+ import { Table } from "../.."
2
+
3
+ /**
4
+ * @type {Table<{ id: number; b_id: number }>}
5
+ */
6
+ export default new Table({
7
+ name: "a",
8
+ priority: 0,
9
+ setup(table) {
10
+ table.increments("id").primary().notNullable()
11
+ table
12
+ .integer("b_id")
13
+ .references("id")
14
+ .inTable("b")
15
+ .onDelete("cascade")
16
+ .notNullable()
17
+ },
18
+ async then({ query }) {
19
+ await query.insert({
20
+ id: 0,
21
+ b_id: 0,
22
+ })
23
+ },
24
+ })
package/tests/tables/b.js CHANGED
@@ -1,24 +1,27 @@
1
- import { Table } from "../.."
2
-
3
- export default new Table({
4
- name: "b",
5
- migrations: {
6
- 0: (table) =>
7
- table
8
- .integer("c_id")
9
- .references("id")
10
- .inTable("c")
11
- .onDelete("cascade")
12
- .notNullable()
13
- },
14
- priority: 1,
15
- setup(table) {
16
- table.increments("id").primary().notNullable()
17
- },
18
- async then({ query }) {
19
- await query.insert({
20
- id: 0,
21
- c_id: 0,
22
- })
23
- },
24
- })
1
+ import { Table } from "../.."
2
+
3
+ /**
4
+ * @type {Table<{ id: number; c_id: number }>}
5
+ */
6
+ export default new Table({
7
+ name: "b",
8
+ migrations: {
9
+ 0: (table) =>
10
+ table
11
+ .integer("c_id")
12
+ .references("id")
13
+ .inTable("c")
14
+ .onDelete("cascade")
15
+ .notNullable(),
16
+ },
17
+ priority: 1,
18
+ setup(table) {
19
+ table.increments("id").primary().notNullable()
20
+ },
21
+ async then({ query }) {
22
+ await query.insert({
23
+ id: 0,
24
+ c_id: 0,
25
+ })
26
+ },
27
+ })
package/tests/tables/c.js CHANGED
@@ -1,12 +1,15 @@
1
- import { Table } from "../.."
2
-
3
- export default new Table({
4
- name: "c",
5
- priority: 2,
6
- setup(table) {
7
- table.increments("id").primary().notNullable()
8
- },
9
- async then({ query }) {
10
- await query.insert({ id: 0 })
11
- },
12
- })
1
+ import { Table } from "../.."
2
+
3
+ /**
4
+ * @type {Table<{ id: number }>}
5
+ */
6
+ export default new Table({
7
+ name: "c",
8
+ priority: 2,
9
+ setup(table) {
10
+ table.increments("id").primary().notNullable()
11
+ },
12
+ async then({ query }) {
13
+ await query.insert({ id: 0 })
14
+ },
15
+ })
package/tests/test.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import dotenv from "dotenv"
2
+ import { rimraf } from "rimraf"
2
3
  import path from "path"
3
4
  import fs from "fs"
4
5
 
@@ -11,7 +12,10 @@ import b from "./tables/b"
11
12
  import c from "./tables/c"
12
13
 
13
14
  const orm = new ORM({
14
- location: path.join("tests", "tables"),
15
+ tableLocation: path.join(process.cwd(), "tests", "tables"),
16
+ backups: {
17
+ location: path.join(process.cwd(), "backups"),
18
+ },
15
19
  })
16
20
 
17
21
  beforeAll(async () => {
@@ -24,7 +28,9 @@ describe("table management", () => {
24
28
  expect(await orm.hasTable("a")).toBeTruthy()
25
29
  expect(await orm.hasTable("b")).toBeTruthy()
26
30
  expect(await orm.hasTable("c")).toBeTruthy()
31
+ })
27
32
 
33
+ test("table cached", async () => {
28
34
  expect(orm.hasCachedTable("migration")).toBeTruthy()
29
35
  expect(orm.hasCachedTable("a")).toBeTruthy()
30
36
  expect(orm.hasCachedTable("b")).toBeTruthy()
@@ -42,6 +48,8 @@ describe("table management", () => {
42
48
  })
43
49
 
44
50
  test("cascade delete", async () => {
51
+ expect(await a.isEmpty()).toBeFalsy()
52
+
45
53
  await c.query.del()
46
54
 
47
55
  expect(await a.isEmpty()).toBeTruthy()
@@ -68,45 +76,51 @@ describe("table column types", () => {
68
76
  })
69
77
  })
70
78
 
71
- // describe("database extraction", () => {
72
- // beforeAll(async () => {
73
- // await c.query.insert({ id: 0 })
74
- // await b.query.insert({
75
- // id: 0,
76
- // c_id: 0,
77
- // })
78
- // await a.query.insert({
79
- // id: 0,
80
- // b_id: 0,
81
- // })
82
- // })
83
- //
84
- // test("extract CSV", async () => {
85
- // await orm.extract()
86
- //
87
- // expect(fs.existsSync("a.csv")).toBeTruthy()
88
- // expect(fs.existsSync("b.csv")).toBeTruthy()
89
- // expect(fs.existsSync("c.csv")).toBeTruthy()
90
- // })
91
- //
92
- // test("empty tables", async () => {
93
- // await a.query.del()
94
- // await b.query.del()
95
- // await c.query.del()
96
- //
97
- // expect(await a.isEmpty()).toBeTruthy()
98
- // expect(await b.isEmpty()).toBeTruthy()
99
- // expect(await c.isEmpty()).toBeTruthy()
100
- // })
101
- //
102
- // test("import CSV", async () => {
103
- // await orm.import()
104
- //
105
- // expect(await a.isEmpty()).toBeFalsy()
106
- // expect(await b.isEmpty()).toBeFalsy()
107
- // expect(await c.isEmpty()).toBeFalsy()
108
- // })
109
- // })
79
+ describe("database extraction", () => {
80
+ beforeAll(async () => {
81
+ await c.query.insert({ id: 0 })
82
+ await b.query.insert({
83
+ id: 0,
84
+ c_id: 0,
85
+ })
86
+ await a.query.insert({
87
+ id: 0,
88
+ b_id: 0,
89
+ })
90
+ })
91
+
92
+ test("extract CSV", async () => {
93
+ await orm.createBackup()
94
+
95
+ expect(
96
+ fs.existsSync(path.join(orm.config.backups.location, "a_chunk_0.csv")),
97
+ ).toBeTruthy()
98
+ expect(
99
+ fs.existsSync(path.join(orm.config.backups.location, "b_chunk_0.csv")),
100
+ ).toBeTruthy()
101
+ expect(
102
+ fs.existsSync(path.join(orm.config.backups.location, "c_chunk_0.csv")),
103
+ ).toBeTruthy()
104
+ })
105
+
106
+ test("empty tables", async () => {
107
+ await a.query.del()
108
+ await b.query.del()
109
+ await c.query.del()
110
+
111
+ expect(await a.isEmpty()).toBeTruthy()
112
+ expect(await b.isEmpty()).toBeTruthy()
113
+ expect(await c.isEmpty()).toBeTruthy()
114
+ })
115
+
116
+ test("import CSV", async () => {
117
+ await orm.restoreBackup()
118
+
119
+ expect(await a.isEmpty()).toBeFalsy()
120
+ expect(await b.isEmpty()).toBeFalsy()
121
+ expect(await c.isEmpty()).toBeFalsy()
122
+ })
123
+ })
110
124
 
111
125
  describe("table getters", () => {
112
126
  test("table info", async () => {
@@ -126,9 +140,72 @@ describe("table getters", () => {
126
140
  })
127
141
  })
128
142
 
143
+ describe("data caching", () => {
144
+ beforeAll(async () => {
145
+ await c.query.del()
146
+ await c.query.insert([{ id: 1 }, { id: 2 }, { id: 3 }])
147
+ await b.query.insert([
148
+ { id: 1, c_id: 1 },
149
+ { id: 2, c_id: 2 },
150
+ { id: 3, c_id: 3 },
151
+ ])
152
+ await a.query.insert([
153
+ { id: 1, b_id: 1 },
154
+ { id: 2, b_id: 2 },
155
+ { id: 3, b_id: 3 },
156
+ ])
157
+ })
158
+
159
+ test("select with caching", async () => {
160
+ const rows = await a.cache.get("all a", (query) => {
161
+ return query.select("*")
162
+ })
163
+
164
+ expect(rows.length).toBe(3)
165
+ })
166
+
167
+ test("insert with caching", async () => {
168
+ await a.cache.set((query) => {
169
+ return query.insert({ id: 4, b_id: 1 })
170
+ })
171
+
172
+ expect(await a.cache.count()).toBe(4)
173
+ })
174
+
175
+ test("update with caching", async () => {
176
+ await a.cache.set((query) => {
177
+ return query.update({ b_id: 3 }).where({ id: 1 })
178
+ })
179
+
180
+ const row = await a.cache.get("a 1", (query) => {
181
+ return query.select("b_id").where({ id: 1 }).first()
182
+ })
183
+
184
+ expect(row.b_id).toBe(3)
185
+ })
186
+
187
+ test("delete with caching", async () => {
188
+ await a.cache.set((query) => {
189
+ return query.delete().where({ id: 1 })
190
+ })
191
+
192
+ expect(await a.cache.count()).toBe(3)
193
+ })
194
+
195
+ test("cache invalidation", async () => {
196
+ expect(await a.cache.count()).toBe(3)
197
+
198
+ await a.query.insert({ id: 5, b_id: 1 })
199
+
200
+ expect(await a.cache.count()).toBe(3)
201
+
202
+ orm.cache.invalidate()
203
+
204
+ expect(await a.cache.count()).toBe(4)
205
+ })
206
+ })
207
+
129
208
  afterAll(async () => {
130
209
  await orm.database.destroy()
131
- // fs.unlinkSync("a.csv")
132
- // fs.unlinkSync("b.csv")
133
- // fs.unlinkSync("c.csv")
210
+ await rimraf(orm.config.backups.location)
134
211
  })
package/tsconfig.json CHANGED
@@ -2,10 +2,13 @@
2
2
  "compilerOptions": {
3
3
  "strict": true,
4
4
  "rootDir": "src",
5
- "target": "es2020",
6
- "module": "CommonJS",
5
+ "outDir": "dist",
6
+ "module": "esnext",
7
+ "target": "esnext",
7
8
  "lib": ["es2020", "dom"],
8
9
  "moduleResolution": "Node",
9
- "esModuleInterop": true
10
- }
10
+ "esModuleInterop": true,
11
+ "typeRoots": ["./node_modules/@types", "./dist/typings"]
12
+ },
13
+ "include": ["src/**/*", "dist/typings/**/*"]
11
14
  }
@@ -1,94 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
25
- var __importDefault = (this && this.__importDefault) || function (mod) {
26
- return (mod && mod.__esModule) ? mod : { "default": mod };
27
- };
28
- Object.defineProperty(exports, "__esModule", { value: true });
29
- exports.ORM = void 0;
30
- const fs_1 = __importDefault(require("fs"));
31
- const url_1 = __importDefault(require("url"));
32
- const path_1 = __importDefault(require("path"));
33
- const handler_1 = require("@ghom/handler");
34
- const knex_1 = __importDefault(require("knex"));
35
- const table_js_1 = require("./table.js");
36
- const defaultBackupDir = path_1.default.join(process.cwd(), "backup");
37
- const pack = JSON.parse(fs_1.default.readFileSync(path_1.default.join(process.cwd(), "package.json"), "utf8"));
38
- const isCJS = pack.type === "commonjs" || pack.type == void 0;
39
- class ORM {
40
- constructor(config) {
41
- this.config = config;
42
- this.database = (0, knex_1.default)(config.database ?? {
43
- client: "sqlite3",
44
- useNullAsDefault: true,
45
- connection: {
46
- filename: ":memory:",
47
- },
48
- });
49
- this.handler = new handler_1.Handler(config.location, {
50
- loader: (filepath) => Promise.resolve(`${isCJS ? filepath : url_1.default.pathToFileURL(filepath).href}`).then(s => __importStar(require(s))).then((file) => file.default),
51
- pattern: /\.js$/,
52
- });
53
- }
54
- get cachedTables() {
55
- return [...this.handler.elements.values()];
56
- }
57
- get cachedTableNames() {
58
- return this.cachedTables.map((table) => table.options.name);
59
- }
60
- hasCachedTable(name) {
61
- return this.cachedTables.some((table) => table.options.name);
62
- }
63
- async hasTable(name) {
64
- return this.database.schema.hasTable(name);
65
- }
66
- /**
67
- * Handle the table files and create the tables in the database.
68
- */
69
- async init() {
70
- await this.handler.init();
71
- try {
72
- await this.database.raw("PRAGMA foreign_keys = ON;");
73
- }
74
- catch (error) { }
75
- const migration = new table_js_1.Table({
76
- name: "migration",
77
- priority: Infinity,
78
- setup: (table) => {
79
- table.string("table").unique().notNullable();
80
- table.integer("version").notNullable();
81
- },
82
- });
83
- migration.orm = this;
84
- await migration.make();
85
- for (const table of this.cachedTables.sort((a, b) => (b.options.priority ?? 0) - (a.options.priority ?? 0))) {
86
- table.orm = this;
87
- await table.make();
88
- }
89
- }
90
- raw(sql) {
91
- return this.database.raw(sql);
92
- }
93
- }
94
- exports.ORM = ORM;