outlet-orm 10.0.0 → 11.1.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.
Files changed (36) hide show
  1. package/README.md +32 -0
  2. package/bin/reverse.js +0 -1
  3. package/package.json +1 -1
  4. package/skills/outlet-orm/ADVANCED.md +29 -0
  5. package/skills/outlet-orm/API.md +30 -0
  6. package/skills/outlet-orm/MODELS.md +144 -2
  7. package/skills/outlet-orm/QUERIES.md +133 -0
  8. package/skills/outlet-orm/RELATIONS.md +44 -0
  9. package/skills/outlet-orm/SKILL.md +4 -2
  10. package/skills/outlet-orm/TYPESCRIPT.md +98 -0
  11. package/src/AI/AIManager.js +58 -58
  12. package/src/AI/AIQueryBuilder.js +2 -2
  13. package/src/AI/AIQueryOptimizer.js +2 -2
  14. package/src/AI/Contracts/AudioProviderContract.js +2 -2
  15. package/src/AI/Contracts/ChatProviderContract.js +3 -2
  16. package/src/AI/Contracts/EmbeddingsProviderContract.js +1 -1
  17. package/src/AI/Contracts/ImageProviderContract.js +1 -1
  18. package/src/AI/Contracts/ModelsProviderContract.js +1 -1
  19. package/src/AI/Contracts/ToolContract.js +1 -1
  20. package/src/AI/MCPServer.js +3 -3
  21. package/src/AI/Providers/CustomOpenAIProvider.js +0 -2
  22. package/src/AI/Providers/GeminiProvider.js +2 -2
  23. package/src/AI/Providers/OpenAIProvider.js +0 -5
  24. package/src/AI/Support/DocumentAttachmentMapper.js +37 -37
  25. package/src/AI/Support/FileSecurity.js +1 -1
  26. package/src/Backup/BackupManager.js +6 -6
  27. package/src/Backup/BackupScheduler.js +1 -1
  28. package/src/Backup/BackupSocketServer.js +2 -2
  29. package/src/DatabaseConnection.js +51 -0
  30. package/src/Model.js +245 -5
  31. package/src/QueryBuilder.js +191 -0
  32. package/src/Relations/HasOneRelation.js +114 -114
  33. package/src/Relations/HasOneThroughRelation.js +105 -105
  34. package/src/Relations/MorphOneRelation.js +4 -2
  35. package/src/Relations/Relation.js +35 -0
  36. package/types/index.d.ts +67 -1
package/src/Model.js CHANGED
@@ -73,6 +73,19 @@ class Model {
73
73
  this.morphMap = map;
74
74
  }
75
75
 
76
+ // Appends: computed attributes included in serialization
77
+ static appends = [];
78
+
79
+ /**
80
+ * Internal own-property names that must never be intercepted by the Proxy.
81
+ * @private
82
+ */
83
+ static _ownProperties = new Set([
84
+ 'attributes', 'original', 'relations', 'touches',
85
+ 'exists', '_showHidden', '_withTrashed', '_onlyTrashed',
86
+ '_instanceVisible', '_instanceHidden', '_changes'
87
+ ]);
88
+
76
89
  constructor(attributes = {}) {
77
90
  // Auto-initialize connection on first model instantiation if missing
78
91
  this.constructor.ensureConnection();
@@ -84,7 +97,35 @@ class Model {
84
97
  this._showHidden = false;
85
98
  this._withTrashed = false;
86
99
  this._onlyTrashed = false;
100
+ this._instanceVisible = null;
101
+ this._instanceHidden = null;
102
+ this._changes = {};
87
103
  this.fill(attributes);
104
+
105
+ // Return a Proxy so that attribute access works as user.name / user.name = 'x'
106
+ return new Proxy(this, {
107
+ get(target, prop, receiver) {
108
+ // Symbols, own properties, and prototype members → normal access
109
+ if (typeof prop === 'symbol' || prop in target || typeof target[prop] === 'function') {
110
+ return Reflect.get(target, prop, receiver);
111
+ }
112
+ // Static / prototype properties that are not functions (e.g. constructor)
113
+ if (Object.prototype.hasOwnProperty.call(target, prop)) {
114
+ return Reflect.get(target, prop, receiver);
115
+ }
116
+ // Everything else → delegate to getAttribute (accessors, casts, relations)
117
+ return target.getAttribute(prop);
118
+ },
119
+ set(target, prop, value, receiver) {
120
+ // Known own properties → direct assignment
121
+ if (target.constructor._ownProperties.has(prop) || prop in target) {
122
+ return Reflect.set(target, prop, value, receiver);
123
+ }
124
+ // Everything else → delegate to setAttribute (mutators, casts)
125
+ target.setAttribute(prop, value);
126
+ return true;
127
+ }
128
+ });
88
129
  }
89
130
 
90
131
  // ==================== Events/Hooks ====================
@@ -469,7 +510,25 @@ class Model {
469
510
  static query() {
470
511
  // Ensure a connection exists even when using static APIs without instantiation
471
512
  this.ensureConnection();
472
- return new QueryBuilder(this);
513
+ const qb = new QueryBuilder(this);
514
+
515
+ // Return a Proxy that recognizes local scopes (static scopeXxx methods on the model)
516
+ return new Proxy(qb, {
517
+ get(target, prop, receiver) {
518
+ if (prop in target || typeof prop === 'symbol') {
519
+ return Reflect.get(target, prop, receiver);
520
+ }
521
+ // Check for a local scope: model.scopeActive => User.query().active()
522
+ const scopeName = 'scope' + prop.charAt(0).toUpperCase() + prop.slice(1);
523
+ if (typeof target.model[scopeName] === 'function') {
524
+ return (...args) => {
525
+ target.model[scopeName](target, ...args);
526
+ return receiver; // keep the proxy chain
527
+ };
528
+ }
529
+ return Reflect.get(target, prop, receiver);
530
+ }
531
+ });
473
532
  }
474
533
 
475
534
  /**
@@ -952,6 +1011,7 @@ class Model {
952
1011
 
953
1012
  this.setAttribute(this.constructor.primaryKey, result.insertId);
954
1013
  this.exists = true;
1014
+ this._changes = { ...this.attributes };
955
1015
  this.original = { ...this.attributes };
956
1016
 
957
1017
  await this.touchParents();
@@ -986,6 +1046,7 @@ class Model {
986
1046
  { wheres: [{ type: 'basic', column: this.constructor.primaryKey, operator: '=', value: this.getAttribute(this.constructor.primaryKey) }] }
987
1047
  );
988
1048
 
1049
+ this._changes = { ...dirty };
989
1050
  this.original = { ...this.attributes };
990
1051
 
991
1052
  await this.touchParents();
@@ -1077,15 +1138,31 @@ class Model {
1077
1138
  * @returns {Object}
1078
1139
  */
1079
1140
  toJSON() {
1080
- const json = { ...this.attributes };
1141
+ let json = { ...this.attributes };
1081
1142
 
1082
- // Hide specified attributes unless _showHidden is true
1083
- if (!this._showHidden) {
1084
- this.constructor.hidden.forEach(key => {
1143
+ // Instance-level visible: keep only these keys
1144
+ if (this._instanceVisible && this._instanceVisible.length > 0) {
1145
+ const visible = new Set(this._instanceVisible);
1146
+ for (const key of Object.keys(json)) {
1147
+ if (!visible.has(key)) delete json[key];
1148
+ }
1149
+ } else if (!this._showHidden) {
1150
+ // Hide specified attributes unless _showHidden is true
1151
+ // If _instanceHidden was explicitly set (via makeVisible/makeHidden), use it as-is
1152
+ const hidden = this._instanceHidden !== null
1153
+ ? new Set(this._instanceHidden)
1154
+ : new Set(this.constructor.hidden);
1155
+ hidden.forEach(key => {
1085
1156
  delete json[key];
1086
1157
  });
1087
1158
  }
1088
1159
 
1160
+ // Appends: include computed attributes via accessors
1161
+ const appends = this.constructor.appends || [];
1162
+ for (const attr of appends) {
1163
+ json[attr] = this.getAttribute(attr);
1164
+ }
1165
+
1089
1166
  // Add relations
1090
1167
  Object.assign(json, this.relations);
1091
1168
 
@@ -1144,6 +1221,169 @@ class Model {
1144
1221
  }
1145
1222
  }
1146
1223
 
1224
+ // ==================== Model Instance Utilities ====================
1225
+
1226
+ /**
1227
+ * Reload a fresh model instance from the database
1228
+ * @param {...string} relations - Relations to eager load
1229
+ * @returns {Promise<Model|null>}
1230
+ */
1231
+ async fresh(...relations) {
1232
+ if (!this.exists) return null;
1233
+ const pk = this.getAttribute(this.constructor.primaryKey);
1234
+ if (pk == null) return null;
1235
+
1236
+ let query = this.constructor.query().where(this.constructor.primaryKey, pk);
1237
+ if (relations.length > 0) {
1238
+ query = query.with(...relations);
1239
+ }
1240
+ return query.first();
1241
+ }
1242
+
1243
+ /**
1244
+ * Reload the model attributes from the database (mutates this instance)
1245
+ * @returns {Promise<this>}
1246
+ */
1247
+ async refresh() {
1248
+ if (!this.exists) return this;
1249
+ const pk = this.getAttribute(this.constructor.primaryKey);
1250
+ if (pk == null) return this;
1251
+
1252
+ const freshModel = await this.constructor.query()
1253
+ .where(this.constructor.primaryKey, pk)
1254
+ .first();
1255
+
1256
+ if (freshModel) {
1257
+ this.attributes = { ...freshModel.attributes };
1258
+ this.original = { ...freshModel.attributes };
1259
+ this.relations = {};
1260
+ }
1261
+ return this;
1262
+ }
1263
+
1264
+ /**
1265
+ * Clone the model into a new, non-existing instance (without primary key)
1266
+ * @param {...string} except - Additional attributes to exclude
1267
+ * @returns {Model}
1268
+ */
1269
+ replicate(...except) {
1270
+ const excluded = new Set([this.constructor.primaryKey, ...except]);
1271
+ const attrs = {};
1272
+ for (const [key, value] of Object.entries(this.attributes)) {
1273
+ if (!excluded.has(key)) {
1274
+ attrs[key] = value;
1275
+ }
1276
+ }
1277
+ const instance = new this.constructor();
1278
+ instance.attributes = attrs;
1279
+ return instance;
1280
+ }
1281
+
1282
+ /**
1283
+ * Determine if two models have the same ID and belong to the same table
1284
+ * @param {Model|null} model
1285
+ * @returns {boolean}
1286
+ */
1287
+ is(model) {
1288
+ if (!model) return false;
1289
+ return this.constructor.table === model.constructor.table &&
1290
+ this.constructor.primaryKey === model.constructor.primaryKey &&
1291
+ this.getAttribute(this.constructor.primaryKey) === model.getAttribute(model.constructor.primaryKey) &&
1292
+ this.getAttribute(this.constructor.primaryKey) != null;
1293
+ }
1294
+
1295
+ /**
1296
+ * Determine if two models are not the same
1297
+ * @param {Model|null} model
1298
+ * @returns {boolean}
1299
+ */
1300
+ isNot(model) {
1301
+ return !this.is(model);
1302
+ }
1303
+
1304
+ /**
1305
+ * Get a subset of the model's attributes as a plain object
1306
+ * @param {...string|Array<string>} keys
1307
+ * @returns {Object}
1308
+ */
1309
+ only(...keys) {
1310
+ const flatKeys = keys.flat();
1311
+ const result = {};
1312
+ for (const key of flatKeys) {
1313
+ result[key] = this.getAttribute(key);
1314
+ }
1315
+ return result;
1316
+ }
1317
+
1318
+ /**
1319
+ * Get all attributes except the specified keys as a plain object
1320
+ * @param {...string|Array<string>} keys
1321
+ * @returns {Object}
1322
+ */
1323
+ except(...keys) {
1324
+ const excluded = new Set(keys.flat());
1325
+ const result = {};
1326
+ for (const [key, value] of Object.entries(this.attributes)) {
1327
+ if (!excluded.has(key)) {
1328
+ result[key] = value;
1329
+ }
1330
+ }
1331
+ return result;
1332
+ }
1333
+
1334
+ /**
1335
+ * Make given attributes visible on this instance (overrides static hidden)
1336
+ * @param {...string|Array<string>} attrs
1337
+ * @returns {this}
1338
+ */
1339
+ makeVisible(...attrs) {
1340
+ const flat = attrs.flat();
1341
+ // Initialize from static hidden if not yet overridden
1342
+ if (!this._instanceHidden) {
1343
+ this._instanceHidden = [...this.constructor.hidden];
1344
+ }
1345
+ this._instanceHidden = this._instanceHidden.filter(k => !flat.includes(k));
1346
+ return this;
1347
+ }
1348
+
1349
+ /**
1350
+ * Make given attributes hidden on this instance
1351
+ * @param {...string|Array<string>} attrs
1352
+ * @returns {this}
1353
+ */
1354
+ makeHidden(...attrs) {
1355
+ const flat = attrs.flat();
1356
+ if (!this._instanceHidden) {
1357
+ this._instanceHidden = [...this.constructor.hidden];
1358
+ }
1359
+ for (const k of flat) {
1360
+ if (!this._instanceHidden.includes(k)) {
1361
+ this._instanceHidden.push(k);
1362
+ }
1363
+ }
1364
+ return this;
1365
+ }
1366
+
1367
+ /**
1368
+ * Determine if the model or a given attribute was changed after the last save
1369
+ * @param {string} [attr]
1370
+ * @returns {boolean}
1371
+ */
1372
+ wasChanged(attr) {
1373
+ if (attr) {
1374
+ return attr in this._changes;
1375
+ }
1376
+ return Object.keys(this._changes).length > 0;
1377
+ }
1378
+
1379
+ /**
1380
+ * Get the attributes that were changed on the last save
1381
+ * @returns {Object}
1382
+ */
1383
+ getChanges() {
1384
+ return { ...this._changes };
1385
+ }
1386
+
1147
1387
  // ==================== Relationships ====================
1148
1388
 
1149
1389
  /**
@@ -924,6 +924,197 @@ class QueryBuilder {
924
924
  }
925
925
  }
926
926
 
927
+ // ==================== Convenience Query Methods ====================
928
+
929
+ /**
930
+ * Get an array of values for a single column, optionally keyed by another column
931
+ * @param {string} column
932
+ * @param {string} [keyColumn]
933
+ * @returns {Promise<Array|Object>}
934
+ */
935
+ async pluck(column, keyColumn) {
936
+ assertIdentifier(column, 'pluck column');
937
+ if (keyColumn) assertIdentifier(keyColumn, 'pluck key column');
938
+
939
+ const cols = keyColumn ? [column, keyColumn] : [column];
940
+ this.selectedColumns = cols;
941
+ this._applyGlobalScopes();
942
+ this._applySoftDeleteConstraints();
943
+
944
+ const rows = await this.model.connection.select(
945
+ this.model.table,
946
+ this.buildQuery()
947
+ );
948
+
949
+ if (keyColumn) {
950
+ const result = {};
951
+ for (const row of rows) {
952
+ result[row[keyColumn]] = row[column];
953
+ }
954
+ return result;
955
+ }
956
+ return rows.map(row => row[column]);
957
+ }
958
+
959
+ /**
960
+ * Get the value of a single column from the first matching row
961
+ * @param {string} column
962
+ * @returns {Promise<any>}
963
+ */
964
+ async value(column) {
965
+ assertIdentifier(column, 'value column');
966
+ this.selectedColumns = [column];
967
+ this._applyGlobalScopes();
968
+ this._applySoftDeleteConstraints();
969
+ this.limitValue = 1;
970
+
971
+ const rows = await this.model.connection.select(
972
+ this.model.table,
973
+ this.buildQuery()
974
+ );
975
+ if (rows.length === 0) return null;
976
+ return rows[0][column];
977
+ }
978
+
979
+ // ==================== Aggregate Methods ====================
980
+
981
+ /**
982
+ * Get the sum of a column
983
+ * @param {string} column
984
+ * @returns {Promise<number>}
985
+ */
986
+ async sum(column) {
987
+ return this._aggregate('SUM', column);
988
+ }
989
+
990
+ /**
991
+ * Get the average of a column
992
+ * @param {string} column
993
+ * @returns {Promise<number>}
994
+ */
995
+ async avg(column) {
996
+ return this._aggregate('AVG', column);
997
+ }
998
+
999
+ /**
1000
+ * Get the minimum value of a column
1001
+ * @param {string} column
1002
+ * @returns {Promise<number>}
1003
+ */
1004
+ async min(column) {
1005
+ return this._aggregate('MIN', column);
1006
+ }
1007
+
1008
+ /**
1009
+ * Get the maximum value of a column
1010
+ * @param {string} column
1011
+ * @returns {Promise<number>}
1012
+ */
1013
+ async max(column) {
1014
+ return this._aggregate('MAX', column);
1015
+ }
1016
+
1017
+ /**
1018
+ * Execute an aggregate function on a column
1019
+ * @param {string} fn - SQL aggregate function
1020
+ * @param {string} column
1021
+ * @returns {Promise<number>}
1022
+ * @private
1023
+ */
1024
+ async _aggregate(fn, column) {
1025
+ assertIdentifier(column, 'aggregate column');
1026
+ this._applyGlobalScopes();
1027
+ this._applySoftDeleteConstraints();
1028
+
1029
+ const result = await this.model.connection.aggregate(
1030
+ this.model.table,
1031
+ fn,
1032
+ column,
1033
+ this.buildQuery()
1034
+ );
1035
+ return result;
1036
+ }
1037
+
1038
+ // ==================== Batch & Conditional Methods ====================
1039
+
1040
+ /**
1041
+ * Process query results in chunks
1042
+ * @param {number} size - Chunk size
1043
+ * @param {Function} callback - Receives (chunk, page). Return false to stop.
1044
+ * @returns {Promise<void>}
1045
+ */
1046
+ async chunk(size, callback) {
1047
+ let page = 1;
1048
+ let offset = 0;
1049
+
1050
+ // eslint-disable-next-line no-constant-condition
1051
+ while (true) {
1052
+ const cloned = this.clone();
1053
+ cloned.limitValue = size;
1054
+ cloned.offsetValue = offset;
1055
+ const results = await cloned.get();
1056
+
1057
+ if (results.length === 0) break;
1058
+
1059
+ const shouldContinue = await callback(results, page);
1060
+ if (shouldContinue === false) break;
1061
+ if (results.length < size) break;
1062
+
1063
+ offset += size;
1064
+ page++;
1065
+ }
1066
+ }
1067
+
1068
+ /**
1069
+ * Apply a callback to the query when a condition is truthy
1070
+ * @param {any} condition
1071
+ * @param {Function} callback - Receives the query builder when condition is truthy
1072
+ * @param {Function} [fallback] - Receives the query builder when condition is falsy
1073
+ * @returns {this}
1074
+ */
1075
+ when(condition, callback, fallback) {
1076
+ if (condition) {
1077
+ callback(this, condition);
1078
+ } else if (typeof fallback === 'function') {
1079
+ fallback(this, condition);
1080
+ }
1081
+ return this;
1082
+ }
1083
+
1084
+ /**
1085
+ * Pass the query builder to a callback for inspection without modifying the chain
1086
+ * @param {Function} callback
1087
+ * @returns {this}
1088
+ */
1089
+ tap(callback) {
1090
+ callback(this);
1091
+ return this;
1092
+ }
1093
+
1094
+ // ==================== Debugging ====================
1095
+
1096
+ /**
1097
+ * Get the raw SQL representation of the current query (for debugging)
1098
+ * @returns {Object} Query object with all clauses
1099
+ */
1100
+ toSQL() {
1101
+ this._applyGlobalScopes();
1102
+ this._applySoftDeleteConstraints();
1103
+ return {
1104
+ table: this.model.table,
1105
+ ...this.buildQuery()
1106
+ };
1107
+ }
1108
+
1109
+ /**
1110
+ * Dump the SQL representation and die (log + throw)
1111
+ */
1112
+ dd() {
1113
+ const sql = this.toSQL();
1114
+ console.log('Query Dump:', JSON.stringify(sql, null, 2));
1115
+ throw new Error('dd(): Query dumped. See console output above.');
1116
+ }
1117
+
927
1118
  /**
928
1119
  * Build the query object
929
1120
  * @returns {Object}