stabilize-orm 1.2.0 → 1.3.2
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/CHANGELOG.md +33 -5
- package/README.md +210 -129
- package/client.ts +7 -82
- package/hooks.ts +65 -22
- package/index.ts +7 -39
- package/migrations.ts +23 -38
- package/model.ts +177 -0
- package/package.json +3 -9
- package/query-builder.ts +27 -4
- package/repository.ts +71 -66
- package/types.ts +1 -0
- package/decorators.ts +0 -170
package/model.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file model.ts
|
|
3
|
+
* @description Provides a programmatic API for defining models and a metadata storage system.
|
|
4
|
+
* @author ElectronSz
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { QueryBuilder } from './query-builder';
|
|
8
|
+
import { DataTypes, RelationType, DBType } from './types';
|
|
9
|
+
|
|
10
|
+
// Interface for column configuration
|
|
11
|
+
export interface ColumnConfig {
|
|
12
|
+
name?: string;
|
|
13
|
+
type: DataTypes;
|
|
14
|
+
length?: number;
|
|
15
|
+
precision?: number;
|
|
16
|
+
scale?: number;
|
|
17
|
+
required?: boolean;
|
|
18
|
+
unique?: boolean;
|
|
19
|
+
defaultValue?: any;
|
|
20
|
+
index?: string; // Optional index name
|
|
21
|
+
softDelete?: boolean; // Marks column as soft delete field
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Interface for relationship configuration
|
|
25
|
+
export interface RelationConfig {
|
|
26
|
+
type: RelationType;
|
|
27
|
+
target: () => any; // Reference to another model
|
|
28
|
+
property: string; // Property name in the model
|
|
29
|
+
foreignKey?: string;
|
|
30
|
+
inverseKey?: string;
|
|
31
|
+
joinTable?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Interface for model configuration
|
|
35
|
+
export interface ModelConfig {
|
|
36
|
+
tableName: string;
|
|
37
|
+
versioned?: boolean;
|
|
38
|
+
softDelete?: boolean;
|
|
39
|
+
columns: Record<string, ColumnConfig>;
|
|
40
|
+
relations?: RelationConfig[];
|
|
41
|
+
scopes?: Record<string, (qb: QueryBuilder<any>, ...args: any[]) => QueryBuilder<any>>; // Custom query scopes
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Metadata storage for models.
|
|
46
|
+
* Stores and retrieves model configuration such as columns, relations, scopes, etc.
|
|
47
|
+
*/
|
|
48
|
+
export class MetadataStorage {
|
|
49
|
+
private static models: Map<Function, ModelConfig> = new Map();
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Associates model metadata with a class constructor.
|
|
53
|
+
* @param model - The class constructor for the model.
|
|
54
|
+
* @param config - The model configuration object.
|
|
55
|
+
*/
|
|
56
|
+
static setModelMetadata(model: Function, config: ModelConfig) {
|
|
57
|
+
this.models.set(model, config);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Retrieves the model configuration for a given model class.
|
|
62
|
+
* @param model - The class constructor for the model.
|
|
63
|
+
* @returns The model configuration or undefined if not found.
|
|
64
|
+
*/
|
|
65
|
+
static getModelMetadata(model: Function): ModelConfig | undefined {
|
|
66
|
+
return this.models.get(model);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Gets the table name for a given model class.
|
|
71
|
+
* @param model - The class constructor for the model.
|
|
72
|
+
* @returns The table name or an empty string if not found.
|
|
73
|
+
*/
|
|
74
|
+
static getTableName(model: Function): string {
|
|
75
|
+
return this.getModelMetadata(model)?.tableName || '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Gets the column configuration for a given model class.
|
|
80
|
+
* @param model - The class constructor for the model.
|
|
81
|
+
* @returns Record of column names to their configuration.
|
|
82
|
+
*/
|
|
83
|
+
static getColumns(model: Function): Record<string, ColumnConfig> {
|
|
84
|
+
return this.getModelMetadata(model)?.columns || {};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Collects validation rules for each column of a given model.
|
|
89
|
+
* @param model - The class constructor for the model.
|
|
90
|
+
* @returns An object mapping column names to an array of validation rule names.
|
|
91
|
+
*/
|
|
92
|
+
static getValidators(model: Function): Record<string, string[]> {
|
|
93
|
+
const columns = this.getModelMetadata(model)?.columns || {};
|
|
94
|
+
const validators: Record<string, string[]> = {};
|
|
95
|
+
for (const [key, col] of Object.entries(columns)) {
|
|
96
|
+
const rules: string[] = [];
|
|
97
|
+
if (col.required) rules.push('required');
|
|
98
|
+
if (col.unique) rules.push('unique');
|
|
99
|
+
validators[key] = rules;
|
|
100
|
+
}
|
|
101
|
+
return validators;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Gets the relationship configuration for a given model class.
|
|
106
|
+
* @param model - The class constructor for the model.
|
|
107
|
+
* @returns Record of property names to their relation configuration.
|
|
108
|
+
*/
|
|
109
|
+
static getRelations(model: Function): Record<string, RelationConfig> {
|
|
110
|
+
const relations = this.getModelMetadata(model)?.relations || [];
|
|
111
|
+
const result: Record<string, RelationConfig> = {};
|
|
112
|
+
for (const rel of relations) {
|
|
113
|
+
result[rel.property] = rel;
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Finds the soft delete field, if any, for a given model class.
|
|
120
|
+
* @param model - The class constructor for the model.
|
|
121
|
+
* @returns The key of the soft delete field, or null if not found.
|
|
122
|
+
*/
|
|
123
|
+
static getSoftDeleteField(model: Function): string | null {
|
|
124
|
+
const columns = this.getModelMetadata(model)?.columns || {};
|
|
125
|
+
for (const [key, col] of Object.entries(columns)) {
|
|
126
|
+
if (col.softDelete) return key;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Checks if the model is versioned.
|
|
133
|
+
* @param model - The class constructor for the model.
|
|
134
|
+
* @returns True if versioned, false otherwise.
|
|
135
|
+
*/
|
|
136
|
+
static isVersioned(model: Function): boolean {
|
|
137
|
+
return !!this.getModelMetadata(model)?.versioned;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Gets custom query scopes for a given model class.
|
|
142
|
+
* @param model - The class constructor for the model.
|
|
143
|
+
* @returns Record of scope names to scope functions.
|
|
144
|
+
*/
|
|
145
|
+
static getScopes(model: Function): Record<string, (qb: QueryBuilder<any>, ...args: any[]) => QueryBuilder<any>> {
|
|
146
|
+
return this.getModelMetadata(model)?.scopes || {};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Programmatically defines a model and stores its metadata.
|
|
152
|
+
* @param config - The model configuration object.
|
|
153
|
+
* @returns The dynamically created model class.
|
|
154
|
+
*/
|
|
155
|
+
export function defineModel(config: ModelConfig) {
|
|
156
|
+
class Model {
|
|
157
|
+
/**
|
|
158
|
+
* Constructs a model instance from plain data.
|
|
159
|
+
* @param data - The plain object to assign properties from.
|
|
160
|
+
*/
|
|
161
|
+
constructor(data: any) {
|
|
162
|
+
Object.assign(this, data);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Store metadata
|
|
167
|
+
MetadataStorage.setModelMetadata(Model, {
|
|
168
|
+
tableName: config.tableName,
|
|
169
|
+
versioned: config.versioned || false,
|
|
170
|
+
softDelete: config.softDelete || false,
|
|
171
|
+
columns: config.columns,
|
|
172
|
+
relations: config.relations || [],
|
|
173
|
+
scopes: config.scopes || {},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return Model;
|
|
177
|
+
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stabilize-orm",
|
|
3
|
-
"version": "1.2
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "A lightweight, type-safe ORM for Bun.js with support for SQLite, MySQL, PostgreSQL, and Redis caching",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
-
"bin": {
|
|
8
|
-
"stabilize": "dist/cli/stabilize-cli.js"
|
|
9
|
-
},
|
|
10
7
|
"scripts": {
|
|
11
|
-
"build": "bun build src/index.ts --outdir dist --target bun --minify
|
|
8
|
+
"build": "bun build src/index.ts --outdir dist --target bun --minify",
|
|
12
9
|
"prepublishOnly": "bun run build && bun run test",
|
|
13
|
-
"
|
|
14
|
-
"format": "bunx prettier --write .",
|
|
15
|
-
"lint": "eslint src tests cli examples",
|
|
16
|
-
"cli": "bun run cli/stabilize-cli.ts"
|
|
10
|
+
"format": "bunx prettier --write ."
|
|
17
11
|
},
|
|
18
12
|
"keywords": [
|
|
19
13
|
"orm",
|
package/query-builder.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import { DBClient } from "./client";
|
|
8
8
|
import { Cache } from "./cache";
|
|
9
|
+
import { MetadataStorage } from "./model";
|
|
10
|
+
import { StabilizeError } from "./types";
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* A fluent interface for building SQL SELECT queries.
|
|
@@ -125,7 +127,7 @@ export class QueryBuilder<T> {
|
|
|
125
127
|
*/
|
|
126
128
|
build(): { query: string; params: any[] } {
|
|
127
129
|
let query = `SELECT ${this.selectFields.join(", ")} FROM ${this.table}`;
|
|
128
|
-
|
|
130
|
+
|
|
129
131
|
if (this.joins.length > 0) {
|
|
130
132
|
query += " " + this.joins.join(" ");
|
|
131
133
|
}
|
|
@@ -166,7 +168,7 @@ export class QueryBuilder<T> {
|
|
|
166
168
|
cacheKey?: string,
|
|
167
169
|
): Promise<T[]> {
|
|
168
170
|
const { query, params } = this.build();
|
|
169
|
-
|
|
171
|
+
|
|
170
172
|
// Attempt to retrieve from cache first (cache-aside read)
|
|
171
173
|
if (cache && cacheKey) {
|
|
172
174
|
const cached = await cache.get<T[]>(cacheKey);
|
|
@@ -178,9 +180,30 @@ export class QueryBuilder<T> {
|
|
|
178
180
|
|
|
179
181
|
// Store the database results in the cache for future requests
|
|
180
182
|
if (cache && cacheKey && results.length > 0) {
|
|
181
|
-
await cache.set(cacheKey, results, 60);
|
|
183
|
+
await cache.set(cacheKey, results, 60);
|
|
182
184
|
}
|
|
183
|
-
|
|
185
|
+
|
|
184
186
|
return results;
|
|
185
187
|
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Applies a named scope to the current query builder.
|
|
191
|
+
*
|
|
192
|
+
* This method looks up a scope function by name for the current model (based on the table name),
|
|
193
|
+
* then invokes the scope function with the query builder and any additional arguments.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} name - The name of the scope to apply.
|
|
196
|
+
* @param {...any} args - Additional arguments to pass to the scope function.
|
|
197
|
+
* @throws {StabilizeError} If no model is found for the current table, or if the specified scope does not exist.
|
|
198
|
+
* @returns {QueryBuilder<T>} The query builder instance after applying the scope.
|
|
199
|
+
*/
|
|
200
|
+
scope(name: string, ...args: any[]): QueryBuilder<T> {
|
|
201
|
+
const model = Object.values(MetadataStorage['models']).find(m => m.tableName === this.table)?.constructor;
|
|
202
|
+
if (!model) throw new StabilizeError(`Model for table ${this.table} not found`, "SCOPE_ERROR");
|
|
203
|
+
const scopes = MetadataStorage.getScopes(model);
|
|
204
|
+
const scopeFn = scopes[name];
|
|
205
|
+
if (!scopeFn) throw new StabilizeError(`Scope ${name} not found`, "SCOPE_ERROR");
|
|
206
|
+
return scopeFn(this, ...args);
|
|
207
|
+
}
|
|
208
|
+
|
|
186
209
|
}
|
package/repository.ts
CHANGED
|
@@ -9,24 +9,17 @@ import { DBClient } from "./client";
|
|
|
9
9
|
import { ConsoleLogger, type Logger } from "./logger";
|
|
10
10
|
import { QueryBuilder } from "./query-builder";
|
|
11
11
|
import {
|
|
12
|
+
DataTypes,
|
|
12
13
|
DBType,
|
|
13
14
|
RelationType,
|
|
14
15
|
StabilizeError,
|
|
15
16
|
type CacheConfig,
|
|
16
17
|
} from "./types";
|
|
17
|
-
import {
|
|
18
|
-
ModelKey,
|
|
19
|
-
ColumnKey,
|
|
20
|
-
ValidatorKey,
|
|
21
|
-
RelationKey,
|
|
22
|
-
SoftDeleteKey,
|
|
23
|
-
VersionedKey,
|
|
24
|
-
} from "./decorators";
|
|
18
|
+
import { MetadataStorage } from "./model";
|
|
25
19
|
import { getHooks, type HookType } from "./hooks";
|
|
26
20
|
|
|
27
21
|
type VersionOperation = "insert" | "update" | "delete";
|
|
28
22
|
|
|
29
|
-
|
|
30
23
|
/**
|
|
31
24
|
* Provides a generic repository for a model `T`.
|
|
32
25
|
* This class abstracts the database interactions for a specific model,
|
|
@@ -57,7 +50,7 @@ export class Repository<T> {
|
|
|
57
50
|
/**
|
|
58
51
|
* Creates an instance of Repository.
|
|
59
52
|
* @param client The database client instance for executing queries.
|
|
60
|
-
* @param model The model class constructor,
|
|
53
|
+
* @param model The model class constructor, defined with `defineModel`.
|
|
61
54
|
* @param cacheConfig Optional configuration for caching.
|
|
62
55
|
* @param logger A logger instance for logging messages.
|
|
63
56
|
*/
|
|
@@ -69,14 +62,30 @@ export class Repository<T> {
|
|
|
69
62
|
) {
|
|
70
63
|
this.client = client;
|
|
71
64
|
this.cache = cacheConfig.enabled ? new Cache(cacheConfig, logger) : null;
|
|
72
|
-
this.table =
|
|
73
|
-
this.columns =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
65
|
+
this.table = MetadataStorage.getTableName(model);
|
|
66
|
+
this.columns = Object.fromEntries(
|
|
67
|
+
Object.entries(MetadataStorage.getColumns(model)).map(([key, col]) => [
|
|
68
|
+
key,
|
|
69
|
+
{ name: col.name ?? key, type: typeof col.type === 'string' ? col.type : DataTypes[col.type] },
|
|
70
|
+
])
|
|
71
|
+
);
|
|
72
|
+
this.relations = Object.fromEntries(
|
|
73
|
+
Object.entries(MetadataStorage.getRelations(model)).map(([key, rel]) => [
|
|
74
|
+
key,
|
|
75
|
+
{
|
|
76
|
+
type: rel.type,
|
|
77
|
+
targetModel: rel.target,
|
|
78
|
+
foreignKey: rel.foreignKey,
|
|
79
|
+
inverseKey: rel.inverseKey,
|
|
80
|
+
joinTable: rel.joinTable,
|
|
81
|
+
},
|
|
82
|
+
])
|
|
83
|
+
);
|
|
84
|
+
this.validators = MetadataStorage.getValidators(model);
|
|
85
|
+
|
|
86
|
+
this.softDeleteField = MetadataStorage.getSoftDeleteField(model);
|
|
78
87
|
this.logger = logger;
|
|
79
|
-
this.versioned =
|
|
88
|
+
this.versioned = MetadataStorage.isVersioned(model);
|
|
80
89
|
this.historyTable = `${this.table}_history`;
|
|
81
90
|
}
|
|
82
91
|
|
|
@@ -93,7 +102,7 @@ export class Repository<T> {
|
|
|
93
102
|
|
|
94
103
|
/**
|
|
95
104
|
* @internal
|
|
96
|
-
* Validates an entity against the 'required' constraints defined in
|
|
105
|
+
* Validates an entity against the 'required' constraints defined in the model configuration.
|
|
97
106
|
* @param entity The partial entity to validate.
|
|
98
107
|
*/
|
|
99
108
|
private validate(entity: Partial<T>) {
|
|
@@ -112,13 +121,13 @@ export class Repository<T> {
|
|
|
112
121
|
}
|
|
113
122
|
|
|
114
123
|
/**
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
124
|
+
* Runs lifecycle hooks of a given type for the entity.
|
|
125
|
+
* @param entity The entity instance.
|
|
126
|
+
* @param type The hook type (e.g., 'beforeCreate').
|
|
127
|
+
*/
|
|
119
128
|
private async runHooks(entity: any, type: HookType): Promise<void> {
|
|
120
129
|
for (const hook of getHooks(entity, type)) {
|
|
121
|
-
await hook();
|
|
130
|
+
await hook.callback(entity);
|
|
122
131
|
}
|
|
123
132
|
}
|
|
124
133
|
|
|
@@ -139,6 +148,21 @@ export class Repository<T> {
|
|
|
139
148
|
return qb;
|
|
140
149
|
}
|
|
141
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Applies a custom scope to the query for the repository's table.
|
|
153
|
+
* @param name The name of the scope to apply.
|
|
154
|
+
* @param args Optional arguments to pass to the scope function.
|
|
155
|
+
* @returns A `QueryBuilder` instance with the scope applied.
|
|
156
|
+
* @example
|
|
157
|
+
* ```
|
|
158
|
+
* const activeUsers = await userRepository.scope('active').execute(client);
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
scope(name: string, ...args: any[]): QueryBuilder<T> {
|
|
162
|
+
this.logger.logDebug(`Applying scope ${name} to ${this.table}`);
|
|
163
|
+
return this.find().scope(name, ...args);
|
|
164
|
+
}
|
|
165
|
+
|
|
142
166
|
/**
|
|
143
167
|
* Finds a single record by its primary key (id).
|
|
144
168
|
* @param id The ID of the record to find.
|
|
@@ -173,8 +197,8 @@ export class Repository<T> {
|
|
|
173
197
|
}
|
|
174
198
|
|
|
175
199
|
/**
|
|
176
|
-
|
|
177
|
-
|
|
200
|
+
* Snapshot query: get record as it was at a point in time.
|
|
201
|
+
*/
|
|
178
202
|
async asOf(
|
|
179
203
|
id: number | string,
|
|
180
204
|
asOfDate: Date,
|
|
@@ -190,8 +214,8 @@ export class Repository<T> {
|
|
|
190
214
|
}
|
|
191
215
|
|
|
192
216
|
/**
|
|
193
|
-
|
|
194
|
-
|
|
217
|
+
* Get all history for a record.
|
|
218
|
+
*/
|
|
195
219
|
async history(
|
|
196
220
|
id: number | string,
|
|
197
221
|
_client?: DBClient
|
|
@@ -204,8 +228,6 @@ export class Repository<T> {
|
|
|
204
228
|
);
|
|
205
229
|
}
|
|
206
230
|
|
|
207
|
-
|
|
208
|
-
|
|
209
231
|
/**
|
|
210
232
|
* Rollback a record to a previous version.
|
|
211
233
|
*/
|
|
@@ -237,21 +259,12 @@ export class Repository<T> {
|
|
|
237
259
|
});
|
|
238
260
|
}
|
|
239
261
|
|
|
240
|
-
|
|
241
262
|
/**
|
|
242
263
|
* Writes a versioned history row for the entity to the history table.
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* This function works for Postgres, MySQL, and SQLite, and uses positional parameters
|
|
249
|
-
* (properly formatted for the target database) for safety and compatibility.
|
|
250
|
-
*
|
|
251
|
-
* @param entity - The entity object being versioned
|
|
252
|
-
* @param operation - The operation performed ("insert", "update", "delete")
|
|
253
|
-
* @param client - The database client to use for the insert
|
|
254
|
-
* @param user - The user/system responsible for the change (default: "system")
|
|
264
|
+
* @param entity The entity object being versioned
|
|
265
|
+
* @param operation The operation performed ("insert", "update", "delete")
|
|
266
|
+
* @param client The database client to use for the insert
|
|
267
|
+
* @param user The user/system responsible for the change (default: "system")
|
|
255
268
|
*/
|
|
256
269
|
private async writeHistory(
|
|
257
270
|
entity: any,
|
|
@@ -261,11 +274,9 @@ export class Repository<T> {
|
|
|
261
274
|
) {
|
|
262
275
|
if (!this.versioned) return;
|
|
263
276
|
|
|
264
|
-
// Get property keys and corresponding SQL column names
|
|
265
277
|
const propertyKeys = Object.keys(this.columns);
|
|
266
278
|
const sqlColumnNames = propertyKeys.map((k) => this.columns[k]!.name);
|
|
267
279
|
|
|
268
|
-
// Build historyColumns using SQL column names
|
|
269
280
|
const historyColumns = [
|
|
270
281
|
...sqlColumnNames,
|
|
271
282
|
"operation",
|
|
@@ -276,9 +287,7 @@ export class Repository<T> {
|
|
|
276
287
|
"modified_at"
|
|
277
288
|
];
|
|
278
289
|
|
|
279
|
-
// Map values from entity using property keys
|
|
280
290
|
const values = propertyKeys.map((k) => entity[k]);
|
|
281
|
-
|
|
282
291
|
const params = [
|
|
283
292
|
...values,
|
|
284
293
|
operation,
|
|
@@ -288,7 +297,7 @@ export class Repository<T> {
|
|
|
288
297
|
user || "system",
|
|
289
298
|
new Date()
|
|
290
299
|
];
|
|
291
|
-
|
|
300
|
+
|
|
292
301
|
let placeholders: string;
|
|
293
302
|
if (client.config.type === DBType.Postgres) {
|
|
294
303
|
placeholders = params.map((_, i) => `$${i + 1}`).join(", ");
|
|
@@ -332,6 +341,7 @@ export class Repository<T> {
|
|
|
332
341
|
return result;
|
|
333
342
|
});
|
|
334
343
|
}
|
|
344
|
+
|
|
335
345
|
/**
|
|
336
346
|
* @internal
|
|
337
347
|
* The private implementation for creating a record, executed within a transaction.
|
|
@@ -407,7 +417,6 @@ export class Repository<T> {
|
|
|
407
417
|
options: { relations?: string[]; batchSize?: number } = {},
|
|
408
418
|
): Promise<T[]> {
|
|
409
419
|
return this.client.transaction(async (txClient) => {
|
|
410
|
-
// Prepare entity instances for hooks
|
|
411
420
|
const preparedEntities = entities.map(data => {
|
|
412
421
|
const instance = new (this as any).model();
|
|
413
422
|
Object.assign(instance, data);
|
|
@@ -464,7 +473,6 @@ export class Repository<T> {
|
|
|
464
473
|
);
|
|
465
474
|
|
|
466
475
|
if (dbType === DBType.Postgres) {
|
|
467
|
-
// PostgreSQL: numbered placeholders ($1, $2, ...)
|
|
468
476
|
let paramIdx = 1;
|
|
469
477
|
const valuePlaceholders = batch
|
|
470
478
|
.map(
|
|
@@ -476,7 +484,6 @@ export class Repository<T> {
|
|
|
476
484
|
const batchResults = await client.query<T>(query, params);
|
|
477
485
|
const ids = batchResults.map((r) => (r as any).id);
|
|
478
486
|
|
|
479
|
-
// Handle relation loading if needed
|
|
480
487
|
let finalResults = batchResults;
|
|
481
488
|
if (ids.length > 0 && batchResults.length === 0) {
|
|
482
489
|
const queryBuilder = this.find().where(
|
|
@@ -493,7 +500,6 @@ export class Repository<T> {
|
|
|
493
500
|
results.push(...finalResults);
|
|
494
501
|
|
|
495
502
|
} else {
|
|
496
|
-
// SQLite/MySQL: ? placeholders
|
|
497
503
|
const placeholders = `(${keys.map(() => "?").join(", ")})`;
|
|
498
504
|
query = `INSERT INTO ${this.table} (${columnNames}) VALUES ${batch.map(() => placeholders).join(", ")}`;
|
|
499
505
|
await client.query(query, params);
|
|
@@ -642,17 +648,14 @@ export class Repository<T> {
|
|
|
642
648
|
for (let i = 0; i < updates.length; i += batchSize) {
|
|
643
649
|
const batch = updates.slice(i, i + batchSize);
|
|
644
650
|
for (const update of batch) {
|
|
645
|
-
// Find all IDs matching the where clause
|
|
646
651
|
const rows = await client.query<{ id: number | string }>(
|
|
647
652
|
`SELECT id FROM ${this.table} WHERE ${update.where.condition}${this.softDeleteField ? ` AND ${this.softDeleteField} IS NULL` : ""}`,
|
|
648
653
|
update.where.params,
|
|
649
654
|
);
|
|
650
655
|
for (const { id } of rows) {
|
|
651
|
-
// Fetch record before update for versioning
|
|
652
656
|
const before = await this.findOne(id, {}, client);
|
|
653
657
|
if (!before) continue;
|
|
654
658
|
|
|
655
|
-
// Prepare instance for hooks
|
|
656
659
|
const instance = new ((this as any).model || Object)();
|
|
657
660
|
Object.assign(instance, before, update.set);
|
|
658
661
|
|
|
@@ -693,6 +696,7 @@ export class Repository<T> {
|
|
|
693
696
|
`Bulk updated ${updates.length} ${this.table} entities in ${(performance.now() - start).toFixed(2)}ms`,
|
|
694
697
|
);
|
|
695
698
|
}
|
|
699
|
+
|
|
696
700
|
/**
|
|
697
701
|
* Performs an "update or insert" operation based on a set of unique keys.
|
|
698
702
|
* @param entity The entity to upsert.
|
|
@@ -728,6 +732,7 @@ export class Repository<T> {
|
|
|
728
732
|
const columns = Object.keys(entity).filter((k) => this.columns[k]);
|
|
729
733
|
const columnNames = columns.map((k) => this.columns[k]?.name).join(", ");
|
|
730
734
|
const placeholders = columns.map(() => "?").join(", ");
|
|
735
|
+
|
|
731
736
|
const updateClause = columns
|
|
732
737
|
.filter((c) => !keys.includes(c))
|
|
733
738
|
.map((c) => `${this.columns[c]?.name} = ?`).join(", ");
|
|
@@ -737,7 +742,6 @@ export class Repository<T> {
|
|
|
737
742
|
const insertParams = columns.map((k) => (entity as any)[k]);
|
|
738
743
|
let params = [...insertParams, ...updateParams];
|
|
739
744
|
|
|
740
|
-
// Try to find the record before upsert
|
|
741
745
|
let before: T | null = null;
|
|
742
746
|
let isUpdate = false;
|
|
743
747
|
if (this.versioned && keys.length > 0) {
|
|
@@ -751,7 +755,6 @@ export class Repository<T> {
|
|
|
751
755
|
isUpdate = !!before;
|
|
752
756
|
}
|
|
753
757
|
|
|
754
|
-
// Prepare instance for hooks
|
|
755
758
|
const instance = new ((this as any).model || Object)();
|
|
756
759
|
Object.assign(instance, before || {}, entity);
|
|
757
760
|
|
|
@@ -767,7 +770,7 @@ export class Repository<T> {
|
|
|
767
770
|
query = `INSERT INTO ${this.table} (${columnNames}) VALUES (${placeholders}) ON CONFLICT(${keys.map((k) => this.columns[k]!.name).join(", ")}) DO UPDATE SET ${updateClause}`;
|
|
768
771
|
} else if (dbType === DBType.MySQL) {
|
|
769
772
|
query = `INSERT INTO ${this.table} (${columnNames}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${updateClause}`;
|
|
770
|
-
} else {
|
|
773
|
+
} else {
|
|
771
774
|
const pgUpdateClause = columns
|
|
772
775
|
.filter((c) => !keys.includes(c))
|
|
773
776
|
.map((c) => `${this.columns[c]?.name} = EXCLUDED.${this.columns[c]?.name}`).join(", ");
|
|
@@ -799,13 +802,6 @@ export class Repository<T> {
|
|
|
799
802
|
await this.runHooks(result, "afterSave");
|
|
800
803
|
}
|
|
801
804
|
|
|
802
|
-
if (this.cache) {
|
|
803
|
-
await this.cache.invalidatePattern(`find:${this.table}:*`);
|
|
804
|
-
if (this.cache.getStrategy() === "write-through") {
|
|
805
|
-
await this.cache.set(`findOne:${this.table}:${id}`, [result], 60);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
|
|
809
805
|
if (this.versioned) {
|
|
810
806
|
await this.writeHistory(
|
|
811
807
|
{ ...result, version: before ? ((before as any).version ? (before as any).version + 1 : 1) : 1 },
|
|
@@ -814,11 +810,19 @@ export class Repository<T> {
|
|
|
814
810
|
);
|
|
815
811
|
}
|
|
816
812
|
|
|
813
|
+
if (this.cache) {
|
|
814
|
+
await this.cache.invalidatePattern(`find:${this.table}:*`);
|
|
815
|
+
if (this.cache.getStrategy() === "write-through") {
|
|
816
|
+
await this.cache.set(`findOne:${this.table}:${id}`, [result], 60);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
817
820
|
this.logger.logDebug(
|
|
818
821
|
`Upserted ${this.table} with ID ${id} in ${(performance.now() - start).toFixed(2)}ms`,
|
|
819
822
|
);
|
|
820
823
|
return result;
|
|
821
824
|
}
|
|
825
|
+
|
|
822
826
|
/**
|
|
823
827
|
* Deletes a record by its ID. Performs a soft delete if enabled on the model.
|
|
824
828
|
* @param id The ID of the record to delete.
|
|
@@ -929,6 +933,7 @@ export class Repository<T> {
|
|
|
929
933
|
`Bulk deleted ${ids.length} ${this.table} entities in ${(performance.now() - start).toFixed(2)}ms`,
|
|
930
934
|
);
|
|
931
935
|
}
|
|
936
|
+
|
|
932
937
|
/**
|
|
933
938
|
* Recovers a soft-deleted record by its ID.
|
|
934
939
|
* Throws an error if soft delete is not enabled on the model.
|
|
@@ -1014,7 +1019,7 @@ export class Repository<T> {
|
|
|
1014
1019
|
"RELATION_ERROR",
|
|
1015
1020
|
);
|
|
1016
1021
|
|
|
1017
|
-
const relatedTable =
|
|
1022
|
+
const relatedTable = MetadataStorage.getTableName(rel.targetModel());
|
|
1018
1023
|
if (
|
|
1019
1024
|
rel.type === RelationType.OneToOne ||
|
|
1020
1025
|
rel.type === RelationType.ManyToOne
|
|
@@ -1040,4 +1045,4 @@ export class Repository<T> {
|
|
|
1040
1045
|
);
|
|
1041
1046
|
}
|
|
1042
1047
|
}
|
|
1043
|
-
}
|
|
1048
|
+
}
|