@oneuptime/common 10.0.68 → 10.0.70

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 (95) hide show
  1. package/Models/DatabaseModels/KubernetesCluster.ts +5 -0
  2. package/Models/DatabaseModels/KubernetesResource.ts +19 -0
  3. package/Server/API/KubernetesResourceAPI.ts +2 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.ts +17 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.ts +134 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  7. package/Server/Services/DatabaseService.ts +19 -4
  8. package/Server/Services/KubernetesResourceService.ts +323 -8
  9. package/Server/Types/Database/QueryHelper.ts +127 -0
  10. package/Server/Types/Database/QueryUtil.ts +244 -0
  11. package/Server/Utils/VM/VMRunner.ts +39 -22
  12. package/Types/BaseDatabase/EndsWith.ts +41 -0
  13. package/Types/BaseDatabase/IncludesAll.ts +45 -0
  14. package/Types/BaseDatabase/IncludesNone.ts +48 -0
  15. package/Types/BaseDatabase/NotContains.ts +41 -0
  16. package/Types/BaseDatabase/StartsWith.ts +41 -0
  17. package/Types/IsolatedVM/ReturnResult.ts +6 -0
  18. package/Types/JSON.ts +20 -0
  19. package/Types/Kubernetes/KubernetesInventoryExtractor.ts +15 -1
  20. package/Types/SerializableObjectDictionary.ts +10 -0
  21. package/UI/Components/Filters/BooleanFilter.tsx +1 -0
  22. package/UI/Components/Filters/DateFilter.tsx +212 -25
  23. package/UI/Components/Filters/DropdownFilter.tsx +1 -0
  24. package/UI/Components/Filters/EntityFilter.tsx +214 -41
  25. package/UI/Components/Filters/FilterViewer.tsx +228 -146
  26. package/UI/Components/Filters/FilterViewerItem.tsx +1 -11
  27. package/UI/Components/Filters/FiltersForm.tsx +148 -97
  28. package/UI/Components/Filters/NumberFilter.tsx +219 -34
  29. package/UI/Components/Filters/OperatorSelector.tsx +91 -0
  30. package/UI/Components/Filters/TextFilter.tsx +182 -71
  31. package/UI/Components/Filters/Types/FilterOperator.ts +73 -0
  32. package/UI/Components/ModelTable/BaseModelTable.tsx +8 -0
  33. package/build/dist/Models/DatabaseModels/KubernetesCluster.js +7 -1
  34. package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
  35. package/build/dist/Models/DatabaseModels/KubernetesResource.js +20 -0
  36. package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
  37. package/build/dist/Server/API/KubernetesResourceAPI.js +2 -0
  38. package/build/dist/Server/API/KubernetesResourceAPI.js.map +1 -1
  39. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.js +12 -0
  40. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776865086264-MigrationName.js.map +1 -0
  41. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js +123 -0
  42. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776881254913-DedupeKubernetesClustersAndAddUniqueIndex.js.map +1 -0
  43. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  44. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  45. package/build/dist/Server/Services/DatabaseService.js +18 -4
  46. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  47. package/build/dist/Server/Services/KubernetesResourceService.js +204 -8
  48. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
  49. package/build/dist/Server/Types/Database/QueryHelper.js +110 -0
  50. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  51. package/build/dist/Server/Types/Database/QueryUtil.js +180 -0
  52. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  53. package/build/dist/Server/Utils/VM/VMRunner.js +33 -19
  54. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  55. package/build/dist/Types/BaseDatabase/EndsWith.js +31 -0
  56. package/build/dist/Types/BaseDatabase/EndsWith.js.map +1 -0
  57. package/build/dist/Types/BaseDatabase/IncludesAll.js +34 -0
  58. package/build/dist/Types/BaseDatabase/IncludesAll.js.map +1 -0
  59. package/build/dist/Types/BaseDatabase/IncludesNone.js +34 -0
  60. package/build/dist/Types/BaseDatabase/IncludesNone.js.map +1 -0
  61. package/build/dist/Types/BaseDatabase/NotContains.js +31 -0
  62. package/build/dist/Types/BaseDatabase/NotContains.js.map +1 -0
  63. package/build/dist/Types/BaseDatabase/StartsWith.js +31 -0
  64. package/build/dist/Types/BaseDatabase/StartsWith.js.map +1 -0
  65. package/build/dist/Types/JSON.js +5 -0
  66. package/build/dist/Types/JSON.js.map +1 -1
  67. package/build/dist/Types/Kubernetes/KubernetesInventoryExtractor.js +7 -1
  68. package/build/dist/Types/Kubernetes/KubernetesInventoryExtractor.js.map +1 -1
  69. package/build/dist/Types/SerializableObjectDictionary.js +10 -0
  70. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  71. package/build/dist/UI/Components/Filters/BooleanFilter.js +1 -1
  72. package/build/dist/UI/Components/Filters/BooleanFilter.js.map +1 -1
  73. package/build/dist/UI/Components/Filters/DateFilter.js +158 -14
  74. package/build/dist/UI/Components/Filters/DateFilter.js.map +1 -1
  75. package/build/dist/UI/Components/Filters/DropdownFilter.js +1 -1
  76. package/build/dist/UI/Components/Filters/DropdownFilter.js.map +1 -1
  77. package/build/dist/UI/Components/Filters/EntityFilter.js +174 -30
  78. package/build/dist/UI/Components/Filters/EntityFilter.js.map +1 -1
  79. package/build/dist/UI/Components/Filters/FilterViewer.js +188 -97
  80. package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
  81. package/build/dist/UI/Components/Filters/FilterViewerItem.js +1 -6
  82. package/build/dist/UI/Components/Filters/FilterViewerItem.js.map +1 -1
  83. package/build/dist/UI/Components/Filters/FiltersForm.js +46 -38
  84. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  85. package/build/dist/UI/Components/Filters/NumberFilter.js +165 -23
  86. package/build/dist/UI/Components/Filters/NumberFilter.js.map +1 -1
  87. package/build/dist/UI/Components/Filters/OperatorSelector.js +41 -0
  88. package/build/dist/UI/Components/Filters/OperatorSelector.js.map +1 -0
  89. package/build/dist/UI/Components/Filters/TextFilter.js +130 -53
  90. package/build/dist/UI/Components/Filters/TextFilter.js.map +1 -1
  91. package/build/dist/UI/Components/Filters/Types/FilterOperator.js +63 -0
  92. package/build/dist/UI/Components/Filters/Types/FilterOperator.js.map +1 -0
  93. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +7 -0
  94. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  95. package/package.json +1 -1
@@ -138,6 +138,48 @@ export default class QueryHelper {
138
138
  );
139
139
  }
140
140
 
141
+ @CaptureSpan()
142
+ public static notContains(name: string): FindWhereProperty<any> {
143
+ name = name.toLowerCase().trim();
144
+ const rid: string = Text.generateRandomText(10);
145
+ return Raw(
146
+ (alias: string) => {
147
+ return `(CAST(${alias} AS TEXT) NOT ILIKE :${rid} OR ${alias} IS NULL)`;
148
+ },
149
+ {
150
+ [rid]: `%${name}%`,
151
+ },
152
+ );
153
+ }
154
+
155
+ @CaptureSpan()
156
+ public static startsWith(name: string): FindWhereProperty<any> {
157
+ name = name.toLowerCase().trim();
158
+ const rid: string = Text.generateRandomText(10);
159
+ return Raw(
160
+ (alias: string) => {
161
+ return `(CAST(${alias} AS TEXT) ILIKE :${rid})`;
162
+ },
163
+ {
164
+ [rid]: `${name}%`,
165
+ },
166
+ );
167
+ }
168
+
169
+ @CaptureSpan()
170
+ public static endsWith(name: string): FindWhereProperty<any> {
171
+ name = name.toLowerCase().trim();
172
+ const rid: string = Text.generateRandomText(10);
173
+ return Raw(
174
+ (alias: string) => {
175
+ return `(CAST(${alias} AS TEXT) ILIKE :${rid})`;
176
+ },
177
+ {
178
+ [rid]: `%${name}`,
179
+ },
180
+ );
181
+ }
182
+
141
183
  @CaptureSpan()
142
184
  public static all(values: Array<string | ObjectID>): FindWhereProperty<any> {
143
185
  values = values.map((value: string | ObjectID) => {
@@ -168,6 +210,91 @@ export default class QueryHelper {
168
210
  return this.in(values); // any and in are the same
169
211
  }
170
212
 
213
+ /**
214
+ * Returns a filter that matches owner rows that are linked to *none* of the
215
+ * provided related entity ids through a many-to-many join table. The
216
+ * returned FindOperator is intended to be applied to the primary id column
217
+ * of the owner entity.
218
+ */
219
+ @CaptureSpan()
220
+ public static noneEntitiesInManyToMany(data: {
221
+ values: Array<string | ObjectID>;
222
+ joinTableName: string;
223
+ ownerColumnName: string;
224
+ relationColumnName: string;
225
+ }): FindWhereProperty<any> {
226
+ const values: Array<string> = data.values.map(
227
+ (value: string | ObjectID) => {
228
+ return value.toString();
229
+ },
230
+ );
231
+
232
+ if (!values || values.length === 0) {
233
+ return Raw(() => {
234
+ return `TRUE = TRUE`;
235
+ }, {});
236
+ }
237
+
238
+ const valuesRid: string = Text.generateRandomText(10);
239
+
240
+ const joinTable: string = data.joinTableName.replace(/"/g, '""');
241
+ const ownerCol: string = data.ownerColumnName.replace(/"/g, '""');
242
+ const relationCol: string = data.relationColumnName.replace(/"/g, '""');
243
+
244
+ return Raw(
245
+ (alias: string) => {
246
+ return `(${alias} NOT IN (SELECT "${joinTable}"."${ownerCol}" FROM "${joinTable}" WHERE "${joinTable}"."${relationCol}" IN (:...${valuesRid})))`;
247
+ },
248
+ {
249
+ [valuesRid]: values,
250
+ },
251
+ );
252
+ }
253
+
254
+ /**
255
+ * Returns a filter that matches owner rows that are linked to *all* of the
256
+ * provided related entity ids through a many-to-many join table. The
257
+ * returned FindOperator is intended to be applied to the primary id column
258
+ * of the owner entity.
259
+ */
260
+ @CaptureSpan()
261
+ public static allEntitiesInManyToMany(data: {
262
+ values: Array<string | ObjectID>;
263
+ joinTableName: string;
264
+ ownerColumnName: string;
265
+ relationColumnName: string;
266
+ }): FindWhereProperty<any> {
267
+ const values: Array<string> = data.values.map(
268
+ (value: string | ObjectID) => {
269
+ return value.toString();
270
+ },
271
+ );
272
+
273
+ if (!values || values.length === 0) {
274
+ return Raw(() => {
275
+ return `TRUE = FALSE`;
276
+ }, {});
277
+ }
278
+
279
+ const valuesRid: string = Text.generateRandomText(10);
280
+ const countRid: string = Text.generateRandomText(10);
281
+
282
+ // Escape identifiers so they can safely be embedded in the SQL string.
283
+ const joinTable: string = data.joinTableName.replace(/"/g, '""');
284
+ const ownerCol: string = data.ownerColumnName.replace(/"/g, '""');
285
+ const relationCol: string = data.relationColumnName.replace(/"/g, '""');
286
+
287
+ return Raw(
288
+ (alias: string) => {
289
+ return `(${alias} IN (SELECT "${joinTable}"."${ownerCol}" FROM "${joinTable}" WHERE "${joinTable}"."${relationCol}" IN (:...${valuesRid}) GROUP BY "${joinTable}"."${ownerCol}" HAVING COUNT(DISTINCT "${joinTable}"."${relationCol}") >= :${countRid}))`;
290
+ },
291
+ {
292
+ [valuesRid]: values,
293
+ [countRid]: values.length,
294
+ },
295
+ );
296
+ }
297
+
171
298
  private static in(
172
299
  values: Array<string | ObjectID | number>,
173
300
  ): FindWhereProperty<any> {
@@ -6,6 +6,11 @@ import GreaterThan from "../../../Types/BaseDatabase/GreaterThan";
6
6
  import GreaterThanOrEqual from "../../../Types/BaseDatabase/GreaterThanOrEqual";
7
7
  import InBetween from "../../../Types/BaseDatabase/InBetween";
8
8
  import Includes from "../../../Types/BaseDatabase/Includes";
9
+ import IncludesAll from "../../../Types/BaseDatabase/IncludesAll";
10
+ import IncludesNone from "../../../Types/BaseDatabase/IncludesNone";
11
+ import StartsWith from "../../../Types/BaseDatabase/StartsWith";
12
+ import EndsWith from "../../../Types/BaseDatabase/EndsWith";
13
+ import NotContains from "../../../Types/BaseDatabase/NotContains";
9
14
  import IsNull from "../../../Types/BaseDatabase/IsNull";
10
15
  import LessThan from "../../../Types/BaseDatabase/LessThan";
11
16
  import LessThanOrEqual from "../../../Types/BaseDatabase/LessThanOrEqual";
@@ -17,12 +22,16 @@ import TableColumnType from "../../../Types/Database/TableColumnType";
17
22
  import { JSONObject } from "../../../Types/JSON";
18
23
  import ObjectID from "../../../Types/ObjectID";
19
24
  import Typeof from "../../../Types/Typeof";
25
+ import { And, DataSource } from "typeorm";
20
26
  import { FindOperator } from "typeorm/find-options/FindOperator";
21
27
  import { CompareType } from "../../../Types/Database/CompareBase";
22
28
  import CaptureSpan from "../../Utils/Telemetry/CaptureSpan";
23
29
  import LessThanOrNull from "../../../Types/BaseDatabase/LessThanOrNull";
24
30
  import GreaterThanOrNull from "../../../Types/BaseDatabase/GreaterThanOrNull";
25
31
  import EqualTo from "../../../Types/BaseDatabase/EqualTo";
32
+ import PostgresAppInstance from "../../Infrastructure/PostgresDatabase";
33
+ import { RelationMetadata } from "typeorm/metadata/RelationMetadata";
34
+ import { EntityMetadata } from "typeorm/metadata/EntityMetadata";
26
35
 
27
36
  export default class QueryUtil {
28
37
  @CaptureSpan()
@@ -111,6 +120,30 @@ export default class QueryUtil {
111
120
  query[key] = QueryHelper.search(
112
121
  (query[key] as Search<string>).toString() as any,
113
122
  ) as any;
123
+ } else if (
124
+ query[key] &&
125
+ query[key] instanceof NotContains &&
126
+ tableColumnMetadata
127
+ ) {
128
+ query[key] = QueryHelper.notContains(
129
+ (query[key] as NotContains<string>).toString() as any,
130
+ ) as any;
131
+ } else if (
132
+ query[key] &&
133
+ query[key] instanceof StartsWith &&
134
+ tableColumnMetadata
135
+ ) {
136
+ query[key] = QueryHelper.startsWith(
137
+ (query[key] as StartsWith<string>).toString() as any,
138
+ ) as any;
139
+ } else if (
140
+ query[key] &&
141
+ query[key] instanceof EndsWith &&
142
+ tableColumnMetadata
143
+ ) {
144
+ query[key] = QueryHelper.endsWith(
145
+ (query[key] as EndsWith<string>).toString() as any,
146
+ ) as any;
114
147
  } else if (
115
148
  query[key] &&
116
149
  query[key] instanceof LessThan &&
@@ -142,6 +175,138 @@ export default class QueryUtil {
142
175
  query[key] = QueryHelper.greaterThan(
143
176
  (query[key] as GreaterThan<CompareType>).toString() as any,
144
177
  ) as any;
178
+ } else if (
179
+ query[key] &&
180
+ query[key] instanceof IncludesAll &&
181
+ tableColumnMetadata
182
+ ) {
183
+ if (tableColumnMetadata.type === TableColumnType.EntityArray) {
184
+ const includesAll: IncludesAll = query[key] as IncludesAll;
185
+ const values: Array<string | ObjectID> = (
186
+ includesAll.values as Array<string | ObjectID | number>
187
+ ).map((item: string | ObjectID | number) => {
188
+ if (
189
+ item !== null &&
190
+ typeof item === Typeof.Object &&
191
+ !(item instanceof ObjectID)
192
+ ) {
193
+ const itemRecord: JSONObject = item as unknown as JSONObject;
194
+ if (itemRecord["_id"]) {
195
+ return itemRecord["_id"] as string;
196
+ }
197
+ }
198
+ return item.toString();
199
+ });
200
+
201
+ const manyToManyMeta: {
202
+ joinTableName: string;
203
+ ownerColumnName: string;
204
+ relationColumnName: string;
205
+ } | null = QueryUtil.getManyToManyRelationMetadata(modelType, key);
206
+
207
+ if (manyToManyMeta && values.length > 0) {
208
+ const subqueryFilter: any = QueryHelper.allEntitiesInManyToMany({
209
+ values,
210
+ joinTableName: manyToManyMeta.joinTableName,
211
+ ownerColumnName: manyToManyMeta.ownerColumnName,
212
+ relationColumnName: manyToManyMeta.relationColumnName,
213
+ });
214
+
215
+ // Remove the relation-based filter so TypeORM does not create a
216
+ // JOIN that would yield OR semantics.
217
+ delete query[key];
218
+
219
+ const existingIdFilter: any = (query as any)._id;
220
+ if (existingIdFilter instanceof FindOperator) {
221
+ (query as any)._id = And(existingIdFilter, subqueryFilter);
222
+ } else if (
223
+ existingIdFilter &&
224
+ typeof existingIdFilter === Typeof.String
225
+ ) {
226
+ (query as any)._id = And(
227
+ QueryHelper.equalTo(existingIdFilter as string),
228
+ subqueryFilter,
229
+ );
230
+ } else {
231
+ (query as any)._id = subqueryFilter;
232
+ }
233
+ } else {
234
+ // Fall back to OR behavior when metadata cannot be resolved.
235
+ query[key] = values as any;
236
+ }
237
+ } else if (tableColumnMetadata.type === TableColumnType.Entity) {
238
+ // Entity (single) columns treat AND as a single match — same as OR.
239
+ query[key] = (query[key] as IncludesAll).values as any;
240
+ } else {
241
+ query[key] = QueryHelper.any(
242
+ (query[key] as IncludesAll).values,
243
+ ) as any;
244
+ }
245
+ } else if (
246
+ query[key] &&
247
+ query[key] instanceof IncludesNone &&
248
+ tableColumnMetadata
249
+ ) {
250
+ if (tableColumnMetadata.type === TableColumnType.EntityArray) {
251
+ const includesNone: IncludesNone = query[key] as IncludesNone;
252
+ const values: Array<string | ObjectID> = (
253
+ includesNone.values as Array<string | ObjectID | number>
254
+ ).map((item: string | ObjectID | number) => {
255
+ if (
256
+ item !== null &&
257
+ typeof item === Typeof.Object &&
258
+ !(item instanceof ObjectID)
259
+ ) {
260
+ const itemRecord: JSONObject = item as unknown as JSONObject;
261
+ if (itemRecord["_id"]) {
262
+ return itemRecord["_id"] as string;
263
+ }
264
+ }
265
+ return item.toString();
266
+ });
267
+
268
+ const manyToManyMeta: {
269
+ joinTableName: string;
270
+ ownerColumnName: string;
271
+ relationColumnName: string;
272
+ } | null = QueryUtil.getManyToManyRelationMetadata(modelType, key);
273
+
274
+ if (manyToManyMeta && values.length > 0) {
275
+ const subqueryFilter: any = QueryHelper.noneEntitiesInManyToMany({
276
+ values,
277
+ joinTableName: manyToManyMeta.joinTableName,
278
+ ownerColumnName: manyToManyMeta.ownerColumnName,
279
+ relationColumnName: manyToManyMeta.relationColumnName,
280
+ });
281
+
282
+ delete query[key];
283
+
284
+ const existingIdFilter: any = (query as any)._id;
285
+ if (existingIdFilter instanceof FindOperator) {
286
+ (query as any)._id = And(existingIdFilter, subqueryFilter);
287
+ } else if (
288
+ existingIdFilter &&
289
+ typeof existingIdFilter === Typeof.String
290
+ ) {
291
+ (query as any)._id = And(
292
+ QueryHelper.equalTo(existingIdFilter as string),
293
+ subqueryFilter,
294
+ );
295
+ } else {
296
+ (query as any)._id = subqueryFilter;
297
+ }
298
+ } else {
299
+ delete query[key];
300
+ }
301
+ } else if (tableColumnMetadata.type === TableColumnType.Entity) {
302
+ query[key] = QueryHelper.notIn(
303
+ (query[key] as IncludesNone).values as Array<string | ObjectID>,
304
+ ) as any;
305
+ } else {
306
+ query[key] = QueryHelper.notIn(
307
+ (query[key] as IncludesNone).values as Array<string | ObjectID>,
308
+ ) as any;
309
+ }
145
310
  } else if (
146
311
  query[key] &&
147
312
  query[key] instanceof Includes &&
@@ -237,4 +402,83 @@ export default class QueryUtil {
237
402
 
238
403
  return query;
239
404
  }
405
+
406
+ /**
407
+ * Resolves the join-table metadata for a many-to-many relation declared on
408
+ * the provided model. Returns null when the column is not a many-to-many
409
+ * relation, the database connection is not yet ready, or required metadata
410
+ * is missing.
411
+ */
412
+ public static getManyToManyRelationMetadata<TBaseModel extends BaseModel>(
413
+ modelType: { new (): TBaseModel },
414
+ propertyPath: string,
415
+ ): {
416
+ joinTableName: string;
417
+ ownerColumnName: string;
418
+ relationColumnName: string;
419
+ } | null {
420
+ if (!PostgresAppInstance.isConnected()) {
421
+ return null;
422
+ }
423
+
424
+ const dataSource: DataSource | null = PostgresAppInstance.getDataSource();
425
+
426
+ if (!dataSource) {
427
+ return null;
428
+ }
429
+
430
+ let entityMetadata: EntityMetadata | undefined;
431
+ try {
432
+ entityMetadata = dataSource.getMetadata(modelType);
433
+ } catch {
434
+ return null;
435
+ }
436
+
437
+ if (!entityMetadata) {
438
+ return null;
439
+ }
440
+
441
+ const relation: RelationMetadata | undefined =
442
+ entityMetadata.findRelationWithPropertyPath(propertyPath);
443
+
444
+ if (!relation || !relation.isManyToMany) {
445
+ return null;
446
+ }
447
+
448
+ // Only the owning side of a many-to-many has join/inverse columns. Follow
449
+ // the inverse relation when needed.
450
+ const owningRelation: RelationMetadata = relation.isOwning
451
+ ? relation
452
+ : (relation.inverseRelation ?? relation);
453
+
454
+ const joinTableName: string | undefined =
455
+ owningRelation.junctionEntityMetadata?.tableName;
456
+
457
+ if (!joinTableName) {
458
+ return null;
459
+ }
460
+
461
+ // When `modelType` is the owning side, its id lives on joinColumns. When
462
+ // it is the inverse side, its id lives on inverseJoinColumns.
463
+ const ownerColumns: Array<any> = relation.isOwning
464
+ ? owningRelation.joinColumns
465
+ : owningRelation.inverseJoinColumns;
466
+ const relationColumns: Array<any> = relation.isOwning
467
+ ? owningRelation.inverseJoinColumns
468
+ : owningRelation.joinColumns;
469
+
470
+ const ownerColumnName: string | undefined = ownerColumns[0]?.databaseName;
471
+ const relationColumnName: string | undefined =
472
+ relationColumns[0]?.databaseName;
473
+
474
+ if (!ownerColumnName || !relationColumnName) {
475
+ return null;
476
+ }
477
+
478
+ return {
479
+ joinTableName,
480
+ ownerColumnName,
481
+ relationColumnName,
482
+ };
483
+ }
240
484
  }
@@ -224,7 +224,7 @@ function createSandboxProxy(
224
224
  * Recursively unwraps sandbox proxies in a return value so the host code
225
225
  * receives original objects (e.g. Buffers that pass `instanceof` checks).
226
226
  */
227
- function deepUnwrapProxies(
227
+ export function deepUnwrapProxies(
228
228
  value: unknown,
229
229
  visited?: WeakSet<GenericObject>,
230
230
  ): unknown {
@@ -469,33 +469,50 @@ export default class VMRunner {
469
469
  })()`;
470
470
 
471
471
  try {
472
- /*
473
- * vm timeout only covers synchronous CPU time, so wrap with
474
- * Promise.race to also cover async operations (network, timers, etc.)
475
- */
476
- const vmPromise: Promise<unknown> = vm.runInContext(script, sandbox, {
477
- timeout: timeout,
478
- });
479
-
480
- const overallTimeout: Promise<never> = new Promise(
481
- (_resolve: (value: never) => void, reject: (reason: Error) => void) => {
482
- const handle: NodeJS.Timeout = global.setTimeout(() => {
483
- reject(new Error("Script execution timed out"));
484
- }, timeout + 5000);
485
- // Don't let this timer keep the process alive
486
- handle.unref();
487
- },
488
- );
472
+ let returnVal: unknown;
473
+ let scriptError: Error | undefined;
474
+
475
+ try {
476
+ /*
477
+ * vm timeout only covers synchronous CPU time, so wrap with
478
+ * Promise.race to also cover async operations (network, timers, etc.)
479
+ */
480
+ const vmPromise: Promise<unknown> = vm.runInContext(script, sandbox, {
481
+ timeout: timeout,
482
+ });
489
483
 
490
- const returnVal: unknown = await Promise.race([
491
- vmPromise,
492
- overallTimeout,
493
- ]);
484
+ const overallTimeout: Promise<never> = new Promise(
485
+ (
486
+ _resolve: (value: never) => void,
487
+ reject: (reason: Error) => void,
488
+ ) => {
489
+ const handle: NodeJS.Timeout = global.setTimeout(() => {
490
+ reject(new Error("Script execution timed out"));
491
+ }, timeout + 5000);
492
+ // Don't let this timer keep the process alive
493
+ handle.unref();
494
+ },
495
+ );
496
+
497
+ returnVal = await Promise.race([vmPromise, overallTimeout]);
498
+ } catch (err: unknown) {
499
+ /*
500
+ * Capture user-thrown errors (including timeouts) so the caller can
501
+ * still access side-channel data collected before the throw — e.g.
502
+ * screenshots assigned to a host-realm object passed via `context`.
503
+ * Rethrowing here would discard those partial results.
504
+ */
505
+ scriptError =
506
+ err instanceof Error
507
+ ? err
508
+ : new Error(typeof err === "string" ? err : String(err));
509
+ }
494
510
 
495
511
  return {
496
512
  returnValue: deepUnwrapProxies(returnVal),
497
513
  logMessages,
498
514
  capturedMetrics,
515
+ scriptError,
499
516
  };
500
517
  } finally {
501
518
  // Clean up any lingering timers to prevent resource leaks
@@ -0,0 +1,41 @@
1
+ import BadDataException from "../Exception/BadDataException";
2
+ import { JSONObject, ObjectType } from "../JSON";
3
+ import QueryOperator from "./QueryOperator";
4
+
5
+ export default class EndsWith<T extends string> extends QueryOperator<T> {
6
+ private _value!: T;
7
+
8
+ public get value(): T {
9
+ return this._value;
10
+ }
11
+
12
+ public set value(v: T) {
13
+ this._value = v;
14
+ }
15
+
16
+ public constructor(value: T) {
17
+ super();
18
+ this.value = value;
19
+ }
20
+
21
+ public override toString(): T {
22
+ return this.value;
23
+ }
24
+
25
+ public override toJSON(): JSONObject {
26
+ return {
27
+ _type: ObjectType.EndsWith,
28
+ value: (this as EndsWith<T>).toString(),
29
+ };
30
+ }
31
+
32
+ public static override fromJSON<T extends string>(
33
+ json: JSONObject,
34
+ ): EndsWith<T> {
35
+ if (json["_type"] === ObjectType.EndsWith) {
36
+ return new EndsWith<T>((json["value"] as T) || ("" as T));
37
+ }
38
+
39
+ throw new BadDataException("Invalid JSON: " + JSON.stringify(json));
40
+ }
41
+ }
@@ -0,0 +1,45 @@
1
+ import BadDataException from "../Exception/BadDataException";
2
+ import { JSONObject, ObjectType } from "../JSON";
3
+ import JSONFunctions from "../JSONFunctions";
4
+ import ObjectID from "../ObjectID";
5
+ import QueryOperator from "./QueryOperator";
6
+
7
+ export type IncludesAllType = Array<string> | Array<ObjectID> | Array<number>;
8
+
9
+ export default class IncludesAll extends QueryOperator<IncludesAllType> {
10
+ private _values: IncludesAllType = [];
11
+
12
+ public get values(): IncludesAllType {
13
+ return this._values;
14
+ }
15
+
16
+ public set values(v: IncludesAllType) {
17
+ this._values = v;
18
+ }
19
+
20
+ public constructor(values: IncludesAllType) {
21
+ super();
22
+ this.values = values;
23
+ }
24
+
25
+ public override toJSON(): JSONObject {
26
+ return {
27
+ _type: ObjectType.IncludesAll,
28
+ value: (this as IncludesAll)._values,
29
+ };
30
+ }
31
+
32
+ public static override fromJSON(json: JSONObject): IncludesAll {
33
+ if (json["_type"] === ObjectType.IncludesAll) {
34
+ const valuesArray: Array<string> = [];
35
+
36
+ for (const value of (json["value"] as Array<string>) || []) {
37
+ valuesArray.push(JSONFunctions.deserializeValue(value) as string);
38
+ }
39
+
40
+ return new IncludesAll(valuesArray);
41
+ }
42
+
43
+ throw new BadDataException("Invalid JSON: " + JSON.stringify(json));
44
+ }
45
+ }
@@ -0,0 +1,48 @@
1
+ import BadDataException from "../Exception/BadDataException";
2
+ import { JSONObject, ObjectType } from "../JSON";
3
+ import JSONFunctions from "../JSONFunctions";
4
+ import ObjectID from "../ObjectID";
5
+ import QueryOperator from "./QueryOperator";
6
+
7
+ export type IncludesNoneType =
8
+ | Array<string>
9
+ | Array<ObjectID>
10
+ | Array<number>;
11
+
12
+ export default class IncludesNone extends QueryOperator<IncludesNoneType> {
13
+ private _values: IncludesNoneType = [];
14
+
15
+ public get values(): IncludesNoneType {
16
+ return this._values;
17
+ }
18
+
19
+ public set values(v: IncludesNoneType) {
20
+ this._values = v;
21
+ }
22
+
23
+ public constructor(values: IncludesNoneType) {
24
+ super();
25
+ this.values = values;
26
+ }
27
+
28
+ public override toJSON(): JSONObject {
29
+ return {
30
+ _type: ObjectType.IncludesNone,
31
+ value: (this as IncludesNone)._values,
32
+ };
33
+ }
34
+
35
+ public static override fromJSON(json: JSONObject): IncludesNone {
36
+ if (json["_type"] === ObjectType.IncludesNone) {
37
+ const valuesArray: Array<string> = [];
38
+
39
+ for (const value of (json["value"] as Array<string>) || []) {
40
+ valuesArray.push(JSONFunctions.deserializeValue(value) as string);
41
+ }
42
+
43
+ return new IncludesNone(valuesArray);
44
+ }
45
+
46
+ throw new BadDataException("Invalid JSON: " + JSON.stringify(json));
47
+ }
48
+ }
@@ -0,0 +1,41 @@
1
+ import BadDataException from "../Exception/BadDataException";
2
+ import { JSONObject, ObjectType } from "../JSON";
3
+ import QueryOperator from "./QueryOperator";
4
+
5
+ export default class NotContains<T extends string> extends QueryOperator<T> {
6
+ private _value!: T;
7
+
8
+ public get value(): T {
9
+ return this._value;
10
+ }
11
+
12
+ public set value(v: T) {
13
+ this._value = v;
14
+ }
15
+
16
+ public constructor(value: T) {
17
+ super();
18
+ this.value = value;
19
+ }
20
+
21
+ public override toString(): T {
22
+ return this.value;
23
+ }
24
+
25
+ public override toJSON(): JSONObject {
26
+ return {
27
+ _type: ObjectType.NotContains,
28
+ value: (this as NotContains<T>).toString(),
29
+ };
30
+ }
31
+
32
+ public static override fromJSON<T extends string>(
33
+ json: JSONObject,
34
+ ): NotContains<T> {
35
+ if (json["_type"] === ObjectType.NotContains) {
36
+ return new NotContains<T>((json["value"] as T) || ("" as T));
37
+ }
38
+
39
+ throw new BadDataException("Invalid JSON: " + JSON.stringify(json));
40
+ }
41
+ }