@shadow-dev/orm 1.0.3 → 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/core/Database.d.ts +11 -2
- package/dist/core/Database.js +98 -15
- package/dist/core/Migration.d.ts +10 -0
- package/dist/core/Migration.js +1 -0
- package/dist/core/Model.d.ts +22 -7
- package/dist/core/Model.js +37 -7
- package/dist/core/Repository.d.ts +12 -4
- package/dist/core/Repository.js +167 -53
- package/dist/core/index.d.ts +4 -3
- package/dist/core/index.js +4 -19
- package/dist/index.d.ts +8 -2
- package/dist/index.js +8 -18
- package/dist/utils/genNewUUID.d.ts +1 -0
- package/dist/utils/genNewUUID.js +4 -0
- package/dist/utils/getNextId.js +18 -19
- package/dist/utils/index.d.ts +4 -3
- package/dist/utils/index.js +4 -19
- package/dist/utils/syncSchema.d.ts +5 -1
- package/dist/utils/syncSchema.js +120 -38
- package/dist/utils/types.js +1 -2
- package/package.json +2 -2
package/dist/core/Database.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import mysql from "mysql2/promise";
|
|
2
|
-
import { Model } from "./Model";
|
|
3
|
-
export declare function initDatabase(config: mysql.PoolOptions
|
|
2
|
+
import { Model } from "./Model.js";
|
|
3
|
+
export declare function initDatabase(config: mysql.PoolOptions, options?: {
|
|
4
|
+
migrations?: {
|
|
5
|
+
path: string;
|
|
6
|
+
auto?: boolean;
|
|
7
|
+
};
|
|
8
|
+
}): Promise<void>;
|
|
4
9
|
export declare function getPool(): mysql.Pool;
|
|
5
10
|
export declare function registerModel<T>(model: Model<T>): void;
|
|
6
11
|
export declare function getAllModels(): Map<string, Model<any>>;
|
|
12
|
+
export declare function exec(sql: string, params?: any[]): Promise<mysql.QueryResult>;
|
|
13
|
+
export declare function query<T = any>(sql: string, params?: any[]): Promise<T>;
|
|
14
|
+
export declare function transaction<T>(fn: (conn: mysql.PoolConnection) => Promise<T>): Promise<T>;
|
|
15
|
+
export declare function runMigrations(dir: string): Promise<void>;
|
package/dist/core/Database.js
CHANGED
|
@@ -1,28 +1,111 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.initDatabase = initDatabase;
|
|
7
|
-
exports.getPool = getPool;
|
|
8
|
-
exports.registerModel = registerModel;
|
|
9
|
-
exports.getAllModels = getAllModels;
|
|
10
1
|
// Database.ts
|
|
11
|
-
|
|
2
|
+
import mysql from "mysql2/promise";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { pathToFileURL } from "url";
|
|
12
6
|
let pool;
|
|
13
7
|
const modelRegistry = new Map();
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
let migrationsPath = null;
|
|
9
|
+
let autoMigrate = false;
|
|
10
|
+
/* ---------------------------------- */
|
|
11
|
+
/* Initialization */
|
|
12
|
+
/* ---------------------------------- */
|
|
13
|
+
export async function initDatabase(config, options) {
|
|
14
|
+
pool = mysql.createPool(config);
|
|
15
|
+
if (options?.migrations) {
|
|
16
|
+
migrationsPath = options.migrations.path;
|
|
17
|
+
autoMigrate = options.migrations.auto ?? false;
|
|
18
|
+
}
|
|
19
|
+
if (autoMigrate && migrationsPath) {
|
|
20
|
+
await runMigrations(migrationsPath);
|
|
21
|
+
}
|
|
16
22
|
}
|
|
17
|
-
function getPool() {
|
|
23
|
+
export function getPool() {
|
|
18
24
|
if (!pool)
|
|
19
25
|
throw new Error("Database not initialized");
|
|
20
26
|
return pool;
|
|
21
27
|
}
|
|
28
|
+
/* ---------------------------------- */
|
|
29
|
+
/* Models */
|
|
30
|
+
/* ---------------------------------- */
|
|
22
31
|
// @ts-expect-error wierd generic errors
|
|
23
|
-
function registerModel(model) {
|
|
32
|
+
export function registerModel(model) {
|
|
24
33
|
modelRegistry.set(model.name, model);
|
|
25
34
|
}
|
|
26
|
-
function getAllModels() {
|
|
35
|
+
export function getAllModels() {
|
|
27
36
|
return modelRegistry;
|
|
28
37
|
}
|
|
38
|
+
/* ---------------------------------- */
|
|
39
|
+
/* Low-level helpers */
|
|
40
|
+
/* ---------------------------------- */
|
|
41
|
+
export async function exec(sql, params) {
|
|
42
|
+
const [result] = await getPool().execute(sql, params);
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
export async function query(sql, params) {
|
|
46
|
+
const [rows] = await getPool().query(sql, params);
|
|
47
|
+
return rows;
|
|
48
|
+
}
|
|
49
|
+
export async function transaction(fn) {
|
|
50
|
+
const conn = await getPool().getConnection();
|
|
51
|
+
try {
|
|
52
|
+
await conn.beginTransaction();
|
|
53
|
+
const result = await fn(conn);
|
|
54
|
+
await conn.commit();
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
await conn.rollback();
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
conn.release();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/* ---------------------------------- */
|
|
66
|
+
/* Migrations */
|
|
67
|
+
/* ---------------------------------- */
|
|
68
|
+
async function ensureMigrationTable() {
|
|
69
|
+
await exec(`
|
|
70
|
+
CREATE TABLE IF NOT EXISTS shadoworm_migrations (
|
|
71
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
72
|
+
name VARCHAR(255) NOT NULL,
|
|
73
|
+
executed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
74
|
+
);
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
async function loadMigrations(dir) {
|
|
78
|
+
if (!fs.existsSync(dir))
|
|
79
|
+
return [];
|
|
80
|
+
const files = fs
|
|
81
|
+
.readdirSync(dir)
|
|
82
|
+
.filter(f => f.endsWith(".js") || f.endsWith(".ts"));
|
|
83
|
+
const migrations = [];
|
|
84
|
+
for (const file of files) {
|
|
85
|
+
const fullPath = path.resolve(dir, file);
|
|
86
|
+
const fileUrl = pathToFileURL(fullPath).href;
|
|
87
|
+
const mod = await import(fileUrl);
|
|
88
|
+
if (!mod.migration) {
|
|
89
|
+
throw new Error(`Migration ${file} does not export 'migration'`);
|
|
90
|
+
}
|
|
91
|
+
migrations.push(mod.migration);
|
|
92
|
+
}
|
|
93
|
+
return migrations.sort((a, b) => a.id.localeCompare(b.id));
|
|
94
|
+
}
|
|
95
|
+
export async function runMigrations(dir) {
|
|
96
|
+
await ensureMigrationTable();
|
|
97
|
+
const applied = await query(`SELECT id FROM shadoworm_migrations`);
|
|
98
|
+
const appliedIds = new Set(applied.map(m => m.id));
|
|
99
|
+
const migrations = await loadMigrations(dir);
|
|
100
|
+
for (const migration of migrations) {
|
|
101
|
+
if (appliedIds.has(migration.id))
|
|
102
|
+
continue;
|
|
103
|
+
await transaction(async (conn) => {
|
|
104
|
+
await migration.up({
|
|
105
|
+
exec: (sql, params) => conn.execute(sql, params),
|
|
106
|
+
query: (sql, params) => conn.query(sql, params).then(([rows]) => rows)
|
|
107
|
+
});
|
|
108
|
+
await conn.execute(`INSERT INTO shadoworm_migrations (id, name) VALUES (?, ?)`, [migration.id, migration.name]);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface Migration {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
up(db: MigrationContext): Promise<void> | void;
|
|
5
|
+
down?: (db: MigrationContext) => Promise<void> | void;
|
|
6
|
+
}
|
|
7
|
+
export interface MigrationContext {
|
|
8
|
+
exec(sql: string, params?: any[]): Promise<any>;
|
|
9
|
+
query<T = any>(sql: string, params?: any[]): Promise<T>;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/core/Model.d.ts
CHANGED
|
@@ -3,22 +3,37 @@ export interface BaseSchema {
|
|
|
3
3
|
data?: any;
|
|
4
4
|
createdAt?: Date;
|
|
5
5
|
}
|
|
6
|
-
export interface ForeignKeyDefinition {
|
|
7
|
-
column: string;
|
|
8
|
-
reference: string;
|
|
9
|
-
}
|
|
10
6
|
export type SimpleFieldType = "string" | "int" | "float" | "boolean" | "json" | "datetime";
|
|
11
7
|
export interface FieldOptions {
|
|
12
8
|
type: SimpleFieldType;
|
|
13
9
|
pk?: boolean;
|
|
10
|
+
autoIncrement?: boolean;
|
|
14
11
|
required?: boolean;
|
|
12
|
+
unique?: boolean;
|
|
15
13
|
default?: any;
|
|
16
14
|
}
|
|
15
|
+
export interface IndexDefinition {
|
|
16
|
+
name?: string;
|
|
17
|
+
columns: string[];
|
|
18
|
+
unique?: boolean;
|
|
19
|
+
}
|
|
17
20
|
export type SchemaValue = SimpleFieldType | FieldOptions;
|
|
18
|
-
export type
|
|
21
|
+
export type NormalizedField = FieldOptions;
|
|
22
|
+
export interface ForeignKeyDefinition {
|
|
23
|
+
column: string;
|
|
24
|
+
references: {
|
|
25
|
+
table: string;
|
|
26
|
+
column: string;
|
|
27
|
+
};
|
|
28
|
+
onDelete?: "CASCADE" | "SET NULL" | "RESTRICT";
|
|
29
|
+
onUpdate?: "CASCADE" | "RESTRICT";
|
|
30
|
+
}
|
|
19
31
|
export declare class Model<T extends Partial<BaseSchema> = BaseSchema> {
|
|
20
32
|
readonly name: string;
|
|
21
|
-
readonly schema: FlexibleSchema<T>;
|
|
22
33
|
readonly foreignKeys: ForeignKeyDefinition[];
|
|
23
|
-
|
|
34
|
+
readonly indexes: IndexDefinition[];
|
|
35
|
+
readonly normalizedSchema: Record<keyof T, NormalizedField>;
|
|
36
|
+
constructor(name: string, schema: Record<keyof T, SchemaValue>, foreignKeys?: ForeignKeyDefinition[], indexes?: IndexDefinition[]);
|
|
37
|
+
private normalizeSchema;
|
|
38
|
+
getPrimaryKey(): keyof T | undefined;
|
|
24
39
|
}
|
package/dist/core/Model.js
CHANGED
|
@@ -1,11 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class Model {
|
|
5
|
-
|
|
1
|
+
/* ---------------------------------- */
|
|
2
|
+
/* Model */
|
|
3
|
+
/* ---------------------------------- */
|
|
4
|
+
export class Model {
|
|
5
|
+
name;
|
|
6
|
+
foreignKeys;
|
|
7
|
+
indexes;
|
|
8
|
+
normalizedSchema;
|
|
9
|
+
constructor(name, schema, foreignKeys = [], indexes = []) {
|
|
6
10
|
this.name = name;
|
|
7
|
-
this.schema = schema;
|
|
8
11
|
this.foreignKeys = foreignKeys;
|
|
12
|
+
this.indexes = indexes;
|
|
13
|
+
this.normalizedSchema = this.normalizeSchema(schema);
|
|
14
|
+
}
|
|
15
|
+
normalizeSchema(schema) {
|
|
16
|
+
const normalized = {};
|
|
17
|
+
for (const key of Object.keys(schema)) {
|
|
18
|
+
const value = schema[key];
|
|
19
|
+
if (typeof value === "string") {
|
|
20
|
+
normalized[key] = {
|
|
21
|
+
type: value,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
normalized[key] = {
|
|
26
|
+
required: false,
|
|
27
|
+
// @ts-expect-error wierd generic errors
|
|
28
|
+
...value,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return normalized;
|
|
33
|
+
}
|
|
34
|
+
getPrimaryKey() {
|
|
35
|
+
for (const key of Object.keys(this.normalizedSchema)) {
|
|
36
|
+
if (this.normalizedSchema[key].pk)
|
|
37
|
+
return key;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
9
40
|
}
|
|
10
41
|
}
|
|
11
|
-
exports.Model = Model;
|
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import { Model } from "./Model";
|
|
1
|
+
import { Model } from "./Model.js";
|
|
2
2
|
export declare class Repository<T extends object> {
|
|
3
3
|
readonly model: Model<T>;
|
|
4
4
|
constructor(model: Model<T>);
|
|
5
5
|
create(data: T): Promise<T>;
|
|
6
|
+
createMany(rows: T[]): Promise<T[]>;
|
|
7
|
+
bulkInsert(rows: T[]): Promise<number>;
|
|
6
8
|
find(where?: Partial<T>): Promise<T[]>;
|
|
7
9
|
findOne(where: Partial<T>): Promise<T | null>;
|
|
8
|
-
update(where: Partial<T>, data: Partial<T>): Promise<T | null>;
|
|
9
|
-
delete(where: Partial<T>): Promise<void>;
|
|
10
10
|
count(where?: Partial<T>): Promise<number>;
|
|
11
11
|
exists(where: Partial<T>): Promise<boolean>;
|
|
12
|
-
|
|
12
|
+
findById(id: any): Promise<T | null>;
|
|
13
|
+
findManyByIds(ids: any[]): Promise<T[]>;
|
|
14
|
+
update(where: Partial<T>, data: Partial<T>): Promise<T | null>;
|
|
15
|
+
updateMany(where: Partial<T>, data: Partial<T>): Promise<number>;
|
|
16
|
+
delete(where: Partial<T>): Promise<void>;
|
|
17
|
+
deleteMany(where: Partial<T>): Promise<number>;
|
|
18
|
+
upsert(data: T): Promise<T>;
|
|
19
|
+
private getInsertableKeys;
|
|
20
|
+
private normalizeWriteValue;
|
|
13
21
|
private buildWhereClause;
|
|
14
22
|
private getPrimaryKeyField;
|
|
15
23
|
}
|
package/dist/core/Repository.js
CHANGED
|
@@ -1,57 +1,115 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Repository = void 0;
|
|
4
1
|
// Repository.ts
|
|
5
|
-
|
|
6
|
-
class Repository {
|
|
2
|
+
import { getPool } from "./Database.js";
|
|
3
|
+
export class Repository {
|
|
4
|
+
model;
|
|
7
5
|
constructor(model) {
|
|
8
6
|
this.model = model;
|
|
9
|
-
// Validate PK at construction
|
|
10
7
|
const pk = this.getPrimaryKeyField();
|
|
11
8
|
if (!pk) {
|
|
12
9
|
throw new Error(`Model "${model.name}" has no primary key defined (pk: true)`);
|
|
13
10
|
}
|
|
14
11
|
}
|
|
12
|
+
/* ---------------------------------- */
|
|
13
|
+
/* CREATE */
|
|
14
|
+
/* ---------------------------------- */
|
|
15
15
|
async create(data) {
|
|
16
|
-
const keys =
|
|
16
|
+
const keys = this.getInsertableKeys(data);
|
|
17
17
|
if (keys.length === 0)
|
|
18
18
|
throw new Error("create(): empty data");
|
|
19
|
-
const sql = `
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
const sql = `
|
|
20
|
+
INSERT INTO \`${this.model.name}\`
|
|
21
|
+
(${keys.map(k => `\`${k}\``).join(",")})
|
|
22
|
+
VALUES (${keys.map(() => "?").join(",")})
|
|
23
|
+
`;
|
|
24
|
+
const values = keys.map(k => this.normalizeWriteValue(k, data[k]));
|
|
25
|
+
const [res] = await getPool().execute(sql, values);
|
|
23
26
|
const pk = this.getPrimaryKeyField();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const row = await this.findOne({ [pk]: data[pk] });
|
|
27
|
-
if (row)
|
|
28
|
-
return row;
|
|
29
|
-
}
|
|
30
|
-
// If PK is auto-increment and insertId is present
|
|
31
|
-
if (pk && res.insertId && res.insertId !== 0) {
|
|
32
|
-
const row = await this.findOne({ [pk]: res.insertId });
|
|
33
|
-
if (row)
|
|
34
|
-
return row;
|
|
35
|
-
}
|
|
36
|
-
// Fallback — return data + insertId if available
|
|
37
|
-
if (res.insertId && res.insertId !== 0) {
|
|
38
|
-
return { ...data, id: res.insertId };
|
|
27
|
+
if (pk && res.insertId) {
|
|
28
|
+
return (await this.findById(res.insertId));
|
|
39
29
|
}
|
|
40
30
|
return data;
|
|
41
31
|
}
|
|
32
|
+
async createMany(rows) {
|
|
33
|
+
if (rows.length === 0)
|
|
34
|
+
return [];
|
|
35
|
+
const keys = this.getInsertableKeys(rows[0]);
|
|
36
|
+
if (keys.length === 0)
|
|
37
|
+
throw new Error("createMany(): empty rows");
|
|
38
|
+
const placeholders = rows
|
|
39
|
+
.map(() => `(${keys.map(() => "?").join(",")})`)
|
|
40
|
+
.join(",");
|
|
41
|
+
const values = rows.flatMap(row => keys.map(k => this.normalizeWriteValue(k, row[k])));
|
|
42
|
+
const sql = `
|
|
43
|
+
INSERT INTO \`${this.model.name}\`
|
|
44
|
+
(${keys.map(k => `\`${k}\``).join(",")})
|
|
45
|
+
VALUES ${placeholders}
|
|
46
|
+
`;
|
|
47
|
+
const [res] = await getPool().execute(sql, values);
|
|
48
|
+
const pk = this.getPrimaryKeyField();
|
|
49
|
+
if (!pk || !res.insertId)
|
|
50
|
+
return rows;
|
|
51
|
+
const ids = rows.map((_, i) => res.insertId + i);
|
|
52
|
+
return this.findManyByIds(ids);
|
|
53
|
+
}
|
|
54
|
+
async bulkInsert(rows) {
|
|
55
|
+
if (rows.length === 0)
|
|
56
|
+
return 0;
|
|
57
|
+
const keys = this.getInsertableKeys(rows[0]);
|
|
58
|
+
const placeholders = rows
|
|
59
|
+
.map(() => `(${keys.map(() => "?").join(",")})`)
|
|
60
|
+
.join(",");
|
|
61
|
+
const values = rows.flatMap(row => keys.map(k => this.normalizeWriteValue(k, row[k])));
|
|
62
|
+
const sql = `
|
|
63
|
+
INSERT INTO \`${this.model.name}\`
|
|
64
|
+
(${keys.map(k => `\`${k}\``).join(",")})
|
|
65
|
+
VALUES ${placeholders}
|
|
66
|
+
`;
|
|
67
|
+
const [res] = await getPool().execute(sql, values);
|
|
68
|
+
return res.affectedRows;
|
|
69
|
+
}
|
|
70
|
+
/* ---------------------------------- */
|
|
71
|
+
/* READ */
|
|
72
|
+
/* ---------------------------------- */
|
|
42
73
|
async find(where = {}) {
|
|
43
74
|
const { sql, params } = this.buildWhereClause(where);
|
|
44
75
|
const query = `SELECT * FROM \`${this.model.name}\` ${sql}`;
|
|
45
|
-
const [rows] = await
|
|
76
|
+
const [rows] = await getPool().execute(query, params);
|
|
46
77
|
return rows;
|
|
47
78
|
}
|
|
48
79
|
async findOne(where) {
|
|
49
80
|
const { sql, params } = this.buildWhereClause(where);
|
|
50
81
|
const query = `SELECT * FROM \`${this.model.name}\` ${sql} LIMIT 1`;
|
|
51
|
-
const [rows] = await
|
|
52
|
-
|
|
53
|
-
|
|
82
|
+
const [rows] = await getPool().execute(query, params);
|
|
83
|
+
return rows[0] ?? null;
|
|
84
|
+
}
|
|
85
|
+
async count(where = {}) {
|
|
86
|
+
const { sql, params } = this.buildWhereClause(where);
|
|
87
|
+
const query = `SELECT COUNT(*) as count FROM \`${this.model.name}\` ${sql}`;
|
|
88
|
+
const [rows] = await getPool().execute(query, params);
|
|
89
|
+
return rows[0]?.count ?? 0;
|
|
90
|
+
}
|
|
91
|
+
async exists(where) {
|
|
92
|
+
return (await this.count(where)) > 0;
|
|
93
|
+
}
|
|
94
|
+
async findById(id) {
|
|
95
|
+
const pk = this.getPrimaryKeyField();
|
|
96
|
+
return this.findOne({ [pk]: id });
|
|
97
|
+
}
|
|
98
|
+
async findManyByIds(ids) {
|
|
99
|
+
if (ids.length === 0)
|
|
100
|
+
return [];
|
|
101
|
+
const pk = this.getPrimaryKeyField();
|
|
102
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
103
|
+
const query = `
|
|
104
|
+
SELECT * FROM \`${this.model.name}\`
|
|
105
|
+
WHERE \`${String(pk)}\` IN (${placeholders})
|
|
106
|
+
`;
|
|
107
|
+
const [rows] = await getPool().execute(query, ids);
|
|
108
|
+
return rows;
|
|
54
109
|
}
|
|
110
|
+
/* ---------------------------------- */
|
|
111
|
+
/* UPDATE */
|
|
112
|
+
/* ---------------------------------- */
|
|
55
113
|
async update(where, data) {
|
|
56
114
|
if (!where || Object.keys(where).length === 0) {
|
|
57
115
|
throw new Error("update(): missing WHERE");
|
|
@@ -60,41 +118,103 @@ class Repository {
|
|
|
60
118
|
if (setKeys.length === 0)
|
|
61
119
|
return this.findOne(where);
|
|
62
120
|
const setClause = setKeys.map(k => `\`${k}\` = ?`).join(", ");
|
|
63
|
-
const setValues = setKeys.map(k => this.
|
|
121
|
+
const setValues = setKeys.map(k => this.normalizeWriteValue(k, data[k]));
|
|
64
122
|
const { sql: whereClause, params: whereValues } = this.buildWhereClause(where);
|
|
65
|
-
const query = `
|
|
66
|
-
|
|
123
|
+
const query = `
|
|
124
|
+
UPDATE \`${this.model.name}\`
|
|
125
|
+
SET ${setClause}
|
|
126
|
+
${whereClause}
|
|
127
|
+
`;
|
|
128
|
+
await getPool().execute(query, [...setValues, ...whereValues]);
|
|
67
129
|
return this.findOne(where);
|
|
68
130
|
}
|
|
131
|
+
async updateMany(where, data) {
|
|
132
|
+
if (!where || Object.keys(where).length === 0) {
|
|
133
|
+
throw new Error("updateMany(): missing WHERE");
|
|
134
|
+
}
|
|
135
|
+
const setKeys = Object.keys(data);
|
|
136
|
+
if (setKeys.length === 0)
|
|
137
|
+
return 0;
|
|
138
|
+
const setClause = setKeys.map(k => `\`${k}\` = ?`).join(", ");
|
|
139
|
+
const setValues = setKeys.map(k => this.normalizeWriteValue(k, data[k]));
|
|
140
|
+
const { sql, params } = this.buildWhereClause(where);
|
|
141
|
+
const query = `
|
|
142
|
+
UPDATE \`${this.model.name}\`
|
|
143
|
+
SET ${setClause}
|
|
144
|
+
${sql}
|
|
145
|
+
`;
|
|
146
|
+
const [res] = await getPool().execute(query, [
|
|
147
|
+
...setValues,
|
|
148
|
+
...params,
|
|
149
|
+
]);
|
|
150
|
+
return res.affectedRows;
|
|
151
|
+
}
|
|
152
|
+
/* ---------------------------------- */
|
|
153
|
+
/* DELETE */
|
|
154
|
+
/* ---------------------------------- */
|
|
69
155
|
async delete(where) {
|
|
70
156
|
const { sql, params } = this.buildWhereClause(where);
|
|
71
157
|
const query = `DELETE FROM \`${this.model.name}\` ${sql}`;
|
|
72
|
-
await
|
|
158
|
+
await getPool().execute(query, params);
|
|
73
159
|
}
|
|
74
|
-
async
|
|
160
|
+
async deleteMany(where) {
|
|
75
161
|
const { sql, params } = this.buildWhereClause(where);
|
|
76
|
-
const query = `
|
|
77
|
-
const [
|
|
78
|
-
return
|
|
162
|
+
const query = `DELETE FROM \`${this.model.name}\` ${sql}`;
|
|
163
|
+
const [res] = await getPool().execute(query, params);
|
|
164
|
+
return res.affectedRows;
|
|
79
165
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
166
|
+
/* ---------------------------------- */
|
|
167
|
+
/* UPSERT */
|
|
168
|
+
/* ---------------------------------- */
|
|
169
|
+
async upsert(data) {
|
|
170
|
+
const pk = this.getPrimaryKeyField();
|
|
171
|
+
const pkField = this.model.normalizedSchema[pk];
|
|
172
|
+
if (pkField?.autoIncrement) {
|
|
173
|
+
throw new Error("upsert() does not support auto-increment primary keys");
|
|
174
|
+
}
|
|
175
|
+
const keys = this.getInsertableKeys(data);
|
|
176
|
+
const insertCols = keys.map(k => `\`${k}\``).join(",");
|
|
177
|
+
const insertVals = keys.map(() => "?").join(",");
|
|
178
|
+
const updateCols = keys
|
|
179
|
+
.filter(k => k !== pk)
|
|
180
|
+
.map(k => `\`${k}\` = VALUES(\`${k}\`)`)
|
|
181
|
+
.join(",");
|
|
182
|
+
const values = keys.map(k => this.normalizeWriteValue(k, data[k]));
|
|
183
|
+
const sql = `
|
|
184
|
+
INSERT INTO \`${this.model.name}\`
|
|
185
|
+
(${insertCols})
|
|
186
|
+
VALUES (${insertVals})
|
|
187
|
+
ON DUPLICATE KEY UPDATE ${updateCols}
|
|
188
|
+
`;
|
|
189
|
+
await getPool().execute(sql, values);
|
|
190
|
+
return (await this.findOne({ [pk]: data[pk] }));
|
|
83
191
|
}
|
|
84
|
-
|
|
85
|
-
|
|
192
|
+
/* ---------------------------------- */
|
|
193
|
+
/* INTERNAL HELPERS */
|
|
194
|
+
/* ---------------------------------- */
|
|
195
|
+
getInsertableKeys(data) {
|
|
196
|
+
return Object.keys(data).filter(key => {
|
|
197
|
+
const field = this.model.normalizedSchema[key];
|
|
198
|
+
return !(field?.pk && field?.autoIncrement);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
normalizeWriteValue(key, value) {
|
|
202
|
+
const field = this.model.normalizedSchema[key];
|
|
203
|
+
if (value == null)
|
|
204
|
+
return null;
|
|
205
|
+
if (field?.type === "datetime" && value instanceof Date) {
|
|
86
206
|
return value.toISOString().slice(0, 19).replace("T", " ");
|
|
87
207
|
}
|
|
88
|
-
if (
|
|
208
|
+
if (field?.type === "json") {
|
|
89
209
|
return JSON.stringify(value);
|
|
90
210
|
}
|
|
91
|
-
return value
|
|
211
|
+
return value;
|
|
92
212
|
}
|
|
93
213
|
buildWhereClause(where) {
|
|
94
214
|
const keys = Object.keys(where);
|
|
95
215
|
if (keys.length === 0)
|
|
96
216
|
return { sql: "", params: [] };
|
|
97
|
-
const conditions = keys.map(k =>
|
|
217
|
+
const conditions = keys.map(k => `\`${k}\` = ?`).join(" AND ");
|
|
98
218
|
const values = keys.map(k => where[k]);
|
|
99
219
|
return {
|
|
100
220
|
sql: `WHERE ${conditions}`,
|
|
@@ -102,12 +222,6 @@ class Repository {
|
|
|
102
222
|
};
|
|
103
223
|
}
|
|
104
224
|
getPrimaryKeyField() {
|
|
105
|
-
|
|
106
|
-
if (this.model.schema[key]?.pk) {
|
|
107
|
-
return key;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return undefined;
|
|
225
|
+
return this.model.getPrimaryKey();
|
|
111
226
|
}
|
|
112
227
|
}
|
|
113
|
-
exports.Repository = Repository;
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export * from './Model';
|
|
2
|
-
export * from './Repository';
|
|
3
|
-
export * from './Database';
|
|
1
|
+
export * from './Model.js';
|
|
2
|
+
export * from './Repository.js';
|
|
3
|
+
export * from './Database.js';
|
|
4
|
+
export * from './Migration.js';
|
package/dist/core/index.js
CHANGED
|
@@ -1,19 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
-
};
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
__exportStar(require("./Model"), exports);
|
|
18
|
-
__exportStar(require("./Repository"), exports);
|
|
19
|
-
__exportStar(require("./Database"), exports);
|
|
1
|
+
export * from './Model.js';
|
|
2
|
+
export * from './Repository.js';
|
|
3
|
+
export * from './Database.js';
|
|
4
|
+
export * from './Migration.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
1
|
+
export * from "./core/Database.js";
|
|
2
|
+
export * from "./core/Model.js";
|
|
3
|
+
export * from "./core/Repository.js";
|
|
4
|
+
export * from "./core/Migration.js";
|
|
5
|
+
export * from './utils/getNextId.js';
|
|
6
|
+
export * from './utils/syncSchema.js';
|
|
7
|
+
export * from './utils/types.js';
|
|
8
|
+
export * from './utils/genNewUUID.js';
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
-
};
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
__exportStar(require("./core"), exports);
|
|
18
|
-
__exportStar(require("./utils"), exports);
|
|
1
|
+
export * from "./core/Database.js";
|
|
2
|
+
export * from "./core/Model.js";
|
|
3
|
+
export * from "./core/Repository.js";
|
|
4
|
+
export * from "./core/Migration.js";
|
|
5
|
+
export * from './utils/getNextId.js';
|
|
6
|
+
export * from './utils/syncSchema.js';
|
|
7
|
+
export * from './utils/types.js';
|
|
8
|
+
export * from './utils/genNewUUID.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function genNewUUID(): string;
|
package/dist/utils/getNextId.js
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
async function getNextId(prefix) {
|
|
6
|
-
const pool = (0, core_1.getPool)();
|
|
1
|
+
import { getPool } from "../core/Database.js";
|
|
2
|
+
export async function getNextId(prefix) {
|
|
3
|
+
const pool = getPool();
|
|
4
|
+
// Ensure table exists (safe to call multiple times)
|
|
7
5
|
await pool.execute(`
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
CREATE TABLE IF NOT EXISTS _id_counters (
|
|
7
|
+
prefix VARCHAR(255) PRIMARY KEY,
|
|
8
|
+
count INT NOT NULL
|
|
9
|
+
)
|
|
10
|
+
`);
|
|
11
|
+
// Atomic upsert + increment
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
13
|
+
const [result] = await pool.execute(`
|
|
14
|
+
INSERT INTO _id_counters (prefix, count)
|
|
15
|
+
VALUES (?, 1)
|
|
16
|
+
ON DUPLICATE KEY UPDATE count = count + 1
|
|
17
|
+
`, [prefix]);
|
|
18
|
+
// Fetch the new value
|
|
13
19
|
const [rows] = await pool.query(`SELECT count FROM _id_counters WHERE prefix = ?`, [prefix]);
|
|
14
|
-
|
|
15
|
-
if (rows.length > 0) {
|
|
16
|
-
count = rows[0].count + 1;
|
|
17
|
-
await pool.execute(`UPDATE _id_counters SET count = ? WHERE prefix = ?`, [count, prefix]);
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
await pool.execute(`INSERT INTO _id_counters (prefix, count) VALUES (?, ?)`, [prefix, count]);
|
|
21
|
-
}
|
|
20
|
+
const count = rows[0].count;
|
|
22
21
|
return `${prefix}-${String(count).padStart(3, "0")}`;
|
|
23
22
|
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export * from './getNextId';
|
|
2
|
-
export * from './syncSchema';
|
|
3
|
-
export * from './types';
|
|
1
|
+
export * from './getNextId.js';
|
|
2
|
+
export * from './syncSchema.js';
|
|
3
|
+
export * from './types.js';
|
|
4
|
+
export * from './genNewUUID.js';
|
package/dist/utils/index.js
CHANGED
|
@@ -1,19 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
-
};
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
__exportStar(require("./getNextId"), exports);
|
|
18
|
-
__exportStar(require("./syncSchema"), exports);
|
|
19
|
-
__exportStar(require("./types"), exports);
|
|
1
|
+
export * from './getNextId.js';
|
|
2
|
+
export * from './syncSchema.js';
|
|
3
|
+
export * from './types.js';
|
|
4
|
+
export * from './genNewUUID.js';
|
package/dist/utils/syncSchema.js
CHANGED
|
@@ -1,51 +1,133 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
// syncSchema.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { getAllModels, getPool } from "../core/Database.js";
|
|
5
|
+
/* ---------------------------------- */
|
|
6
|
+
/* Helpers */
|
|
7
|
+
/* ---------------------------------- */
|
|
8
|
+
function mapType(type) {
|
|
9
|
+
switch (type) {
|
|
10
|
+
case "string": return "VARCHAR(255)";
|
|
11
|
+
case "json": return "JSON";
|
|
12
|
+
case "datetime": return "DATETIME";
|
|
13
|
+
case "int": return "INT";
|
|
14
|
+
case "float": return "FLOAT";
|
|
15
|
+
case "boolean": return "BOOLEAN";
|
|
16
|
+
default: return type;
|
|
17
|
+
}
|
|
9
18
|
}
|
|
10
19
|
function formatDefault(value) {
|
|
11
20
|
if (typeof value === "string")
|
|
12
21
|
return `'${value}'`;
|
|
13
22
|
if (typeof value === "boolean")
|
|
14
23
|
return value ? "TRUE" : "FALSE";
|
|
15
|
-
if (value
|
|
24
|
+
if (value == null)
|
|
16
25
|
return "NULL";
|
|
17
26
|
return value.toString();
|
|
18
27
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
default: return type;
|
|
28
|
-
}
|
|
28
|
+
/* ---------------------------------- */
|
|
29
|
+
/* DB Introspection */
|
|
30
|
+
/* ---------------------------------- */
|
|
31
|
+
async function getExistingTables() {
|
|
32
|
+
const pool = getPool();
|
|
33
|
+
const [rows] = await pool.query("SHOW TABLES");
|
|
34
|
+
// @ts-expect-error wierd generic errors
|
|
35
|
+
return new Set(Object.values(rows[0] ?? {}).length ? rows.map(r => Object.values(r)[0]) : []);
|
|
29
36
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
/* ---------------------------------- */
|
|
38
|
+
/* Migration Generator */
|
|
39
|
+
/* ---------------------------------- */
|
|
40
|
+
export async function syncSchema(options) {
|
|
41
|
+
const generate = options?.generate ?? true;
|
|
42
|
+
const apply = options?.apply ?? false;
|
|
43
|
+
const migrationsPath = options?.migrationsPath ?? "./migrations";
|
|
44
|
+
if (process.env.NODE_ENV === "production" && apply) {
|
|
45
|
+
throw new Error("syncSchema(): cannot apply schema changes in production");
|
|
46
|
+
}
|
|
47
|
+
const models = getAllModels();
|
|
48
|
+
const pool = getPool();
|
|
49
|
+
const existingTables = await getExistingTables();
|
|
50
|
+
const statements = [];
|
|
51
|
+
for (const [tableName, model] of models.entries()) {
|
|
52
|
+
if (existingTables.has(tableName))
|
|
53
|
+
continue;
|
|
54
|
+
statements.push(generateCreateTableSQL(model));
|
|
55
|
+
statements.push(...generateIndexSQL(model));
|
|
56
|
+
}
|
|
57
|
+
if (statements.length === 0) {
|
|
58
|
+
console.log("✅ Schema already in sync.");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (generate) {
|
|
62
|
+
emitMigration(statements, migrationsPath);
|
|
63
|
+
}
|
|
64
|
+
if (apply) {
|
|
65
|
+
for (const sql of statements) {
|
|
66
|
+
await pool.execute(sql);
|
|
45
67
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
68
|
+
console.log("✅ Schema applied (dev mode).");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/* ---------------------------------- */
|
|
72
|
+
/* SQL Builders */
|
|
73
|
+
/* ---------------------------------- */
|
|
74
|
+
function generateCreateTableSQL(model) {
|
|
75
|
+
const columns = [];
|
|
76
|
+
for (const [key, field] of Object.entries(model.normalizedSchema)) {
|
|
77
|
+
let col = `\`${key}\` ${mapType(field.type)}`;
|
|
78
|
+
if (field.pk)
|
|
79
|
+
col += " PRIMARY KEY";
|
|
80
|
+
if (field.autoIncrement)
|
|
81
|
+
col += " AUTO_INCREMENT";
|
|
82
|
+
if (field.required || field.pk)
|
|
83
|
+
col += " NOT NULL";
|
|
84
|
+
if (field.default !== undefined)
|
|
85
|
+
col += ` DEFAULT ${formatDefault(field.default)}`;
|
|
86
|
+
columns.push(col);
|
|
87
|
+
}
|
|
88
|
+
const fks = model.foreignKeys.map(fk => `FOREIGN KEY (\`${fk.column}\`) REFERENCES \`${fk.references.table}\`(\`${fk.references.column}\`)`);
|
|
89
|
+
return `
|
|
90
|
+
CREATE TABLE \`${model.name}\` (
|
|
91
|
+
${[...columns, ...fks].join(",\n ")}
|
|
92
|
+
);`.trim();
|
|
93
|
+
}
|
|
94
|
+
function generateIndexSQL(model) {
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
96
|
+
return model.indexes.map((idx, i) => {
|
|
97
|
+
const name = idx.name ??
|
|
98
|
+
`idx_${model.name}_${idx.columns.join("_")}`;
|
|
99
|
+
const unique = idx.unique ? "UNIQUE " : "";
|
|
100
|
+
const cols = idx.columns.map(c => `\`${c}\``).join(", ");
|
|
101
|
+
return `CREATE ${unique}INDEX \`${name}\` ON \`${model.name}\` (${cols});`;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/* ---------------------------------- */
|
|
105
|
+
/* Migration Writer */
|
|
106
|
+
/* ---------------------------------- */
|
|
107
|
+
function emitMigration(sql, dir) {
|
|
108
|
+
if (!fs.existsSync(dir)) {
|
|
109
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
49
110
|
}
|
|
50
|
-
|
|
111
|
+
const id = new Date()
|
|
112
|
+
.toISOString()
|
|
113
|
+
.replace(/[-:T.Z]/g, "")
|
|
114
|
+
.slice(0, 14);
|
|
115
|
+
const filename = `${id}_auto_sync.ts`;
|
|
116
|
+
const filePath = path.join(dir, filename);
|
|
117
|
+
const content = `
|
|
118
|
+
import type { Migration } from "@shadow-dev/orm";
|
|
119
|
+
|
|
120
|
+
export const migration: Migration = {
|
|
121
|
+
id: "${id}",
|
|
122
|
+
name: "auto_sync",
|
|
123
|
+
|
|
124
|
+
async up(db) {
|
|
125
|
+
${sql
|
|
126
|
+
.map(s => ` await db.exec(\`${s.replace(/`/g, "\\`")}\`);`)
|
|
127
|
+
.join("\n")}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
`.trim();
|
|
131
|
+
fs.writeFileSync(filePath, content, { encoding: "utf8" });
|
|
132
|
+
console.log(`📝 Migration generated: ${filePath}`);
|
|
51
133
|
}
|
package/dist/utils/types.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shadow-dev/orm",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Lightweight dynamic MySQL ORM designed for ShadowCore and modular apps.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"files": ["dist"],
|
|
8
|
+
"type": "module",
|
|
8
9
|
"exports": {
|
|
9
10
|
".": {
|
|
10
11
|
"import": "./dist/index.js",
|
|
11
|
-
"require": "./dist/index.js",
|
|
12
12
|
"types": "./dist/index.d.ts"
|
|
13
13
|
}
|
|
14
14
|
},
|