arkormx 1.2.0 → 1.2.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/dist/cli.mjs CHANGED
@@ -549,7 +549,7 @@ var TableBuilder = class {
549
549
  */
550
550
  foreign(column) {
551
551
  const columnName = this.resolveColumn(column).name;
552
- return this.foreignKey(columnName + (column ? "" : "Id"));
552
+ return this.foreignKey(column ?? columnName);
553
553
  }
554
554
  /**
555
555
  * Returns a deep copy of the defined columns for the table.
@@ -967,24 +967,23 @@ const deriveRelationFieldName = (columnName) => {
967
967
  if (trimmed.endsWith("_id") && trimmed.length > 3) return trimmed.slice(0, -3).replace(/_([a-zA-Z0-9])/g, (_, letter) => letter.toUpperCase());
968
968
  return `${trimmed.charAt(0).toLowerCase()}${trimmed.slice(1)}`;
969
969
  };
970
- const pascalWords = (value) => {
971
- return value.match(/[A-Z][a-z0-9]*/g) ?? [value];
972
- };
973
970
  /**
974
- * Derive a relation name for the inverse side of a relation based on the
971
+ * Derive a relation name for both sides of a relation based on the
975
972
  * source and target model names, using an explicit alias if provided or a
976
- * convention of combining the target model name with the last segment of
977
- * the source model name.
973
+ * convention of combining the full source model name with the target model name.
978
974
  *
979
975
  * @param sourceModelName The name of the source model in the relation.
980
976
  * @param targetModelName The name of the target model in the relation.
981
- * @param explicitAlias An optional explicit alias for the inverse relation.
982
- * @returns The derived or explicit inverse relation alias.
977
+ * @param explicitAlias An optional explicit alias for the relation.
978
+ * @returns The derived or explicit relation alias.
983
979
  */
984
- const deriveInverseRelationAlias = (sourceModelName, targetModelName, explicitAlias) => {
980
+ const deriveRelationAlias = (sourceModelName, targetModelName, explicitAlias) => {
985
981
  if (explicitAlias && explicitAlias.trim().length > 0) return explicitAlias.trim();
986
- const sourceWords = pascalWords(sourceModelName);
987
- return `${sourceWords[sourceWords.length - 1] ?? sourceModelName}${targetModelName}`;
982
+ return [sourceModelName, targetModelName].sort((left, right) => left.localeCompare(right)).join("");
983
+ };
984
+ const deriveSingularFieldName = (modelName) => {
985
+ if (!modelName) return "item";
986
+ return `${modelName.charAt(0).toLowerCase()}${modelName.slice(1)}`;
988
987
  };
989
988
  const deriveCollectionFieldName = (modelName) => {
990
989
  if (!modelName) return "items";
@@ -992,6 +991,12 @@ const deriveCollectionFieldName = (modelName) => {
992
991
  if (camel.endsWith("s")) return `${camel}es`;
993
992
  return `${camel}s`;
994
993
  };
994
+ const resolveForeignKeyColumn = (columns, foreignKey) => {
995
+ return columns.find((column) => column.name === foreignKey.column);
996
+ };
997
+ const isOneToOneForeignKey = (column) => {
998
+ return Boolean(column?.unique || column?.primary);
999
+ };
995
1000
  /**
996
1001
  * Format a SchemaForeignKeyAction value as a Prisma onDelete action string.
997
1002
  *
@@ -1012,15 +1017,16 @@ const formatRelationAction = (action) => {
1012
1017
  * @param foreignKey The foreign key definition to convert to a relation line.
1013
1018
  * @returns The corresponding Prisma schema line for the relation field.
1014
1019
  */
1015
- const buildRelationLine = (foreignKey) => {
1020
+ const buildRelationLine = (sourceModelName, foreignKey, columns = []) => {
1016
1021
  if (!foreignKey.referencesTable.trim()) throw new ArkormException(`Foreign key [${foreignKey.column}] must define a referenced table.`);
1017
1022
  if (!foreignKey.referencesColumn.trim()) throw new ArkormException(`Foreign key [${foreignKey.column}] must define a referenced column.`);
1023
+ const sourceColumn = resolveForeignKeyColumn(columns, foreignKey);
1018
1024
  const fieldName = foreignKey.fieldAlias?.trim() || deriveRelationFieldName(foreignKey.column);
1019
1025
  const targetModel = toModelName(foreignKey.referencesTable);
1020
- const relationName = foreignKey.relationAlias?.trim();
1021
- const relationPrefix = relationName ? `@relation("${relationName.replace(/"/g, "\\\"")}", ` : "@relation(";
1026
+ const relationName = deriveRelationAlias(sourceModelName, targetModel, foreignKey.relationAlias?.trim());
1027
+ const optional = sourceColumn?.nullable ? "?" : "";
1022
1028
  const onDelete = foreignKey.onDelete ? `, onDelete: ${formatRelationAction(foreignKey.onDelete)}` : "";
1023
- return ` ${fieldName} ${targetModel} ${relationPrefix}fields: [${foreignKey.column}], references: [${foreignKey.referencesColumn}]${onDelete})`;
1029
+ return ` ${fieldName} ${targetModel}${optional} @relation("${relationName.replace(/"/g, "\\\"")}", fields: [${foreignKey.column}], references: [${foreignKey.referencesColumn}]${onDelete})`;
1024
1030
  };
1025
1031
  /**
1026
1032
  * Build a Prisma relation field line for the inverse side of a relation, based
@@ -1032,8 +1038,11 @@ const buildRelationLine = (foreignKey) => {
1032
1038
  * @param foreignKey The foreign key definition for the relation.
1033
1039
  * @returns The Prisma schema line for the inverse relation field.
1034
1040
  */
1035
- const buildInverseRelationLine = (sourceModelName, targetModelName, foreignKey) => {
1036
- return ` ${deriveCollectionFieldName(sourceModelName)} ${sourceModelName}[] @relation("${deriveInverseRelationAlias(sourceModelName, targetModelName, foreignKey.inverseRelationAlias).replace(/"/g, "\\\"")}")`;
1041
+ const buildInverseRelationLine = (sourceModelName, targetModelName, foreignKey, columns = []) => {
1042
+ const sourceColumn = resolveForeignKeyColumn(columns, foreignKey);
1043
+ const fieldName = isOneToOneForeignKey(sourceColumn) ? deriveSingularFieldName(sourceModelName) : deriveCollectionFieldName(sourceModelName);
1044
+ const relationName = deriveRelationAlias(sourceModelName, targetModelName, foreignKey.relationAlias?.trim());
1045
+ return ` ${fieldName} ${isOneToOneForeignKey(sourceColumn) ? `${sourceModelName}?` : `${sourceModelName}[]`} @relation("${relationName.replace(/"/g, "\\\"")}")`;
1037
1046
  };
1038
1047
  /**
1039
1048
  * Inject a line into the body of a Prisma model block if it does not already
@@ -1061,14 +1070,15 @@ const injectLineIntoModelBody = (bodyLines, line, exists) => {
1061
1070
  * @param foreignKeys An array of foreign key definitions to process.
1062
1071
  * @returns The updated Prisma schema string with inverse relations applied.
1063
1072
  */
1064
- const applyInverseRelations = (schema, sourceModelName, foreignKeys) => {
1073
+ const applyInverseRelations = (schema, sourceModelName, foreignKeys, columns = []) => {
1065
1074
  let nextSchema = schema;
1066
1075
  for (const foreignKey of foreignKeys) {
1067
1076
  const targetModel = findModelBlock(nextSchema, foreignKey.referencesTable);
1068
1077
  if (!targetModel) continue;
1069
- const inverseLine = buildInverseRelationLine(sourceModelName, targetModel.modelName, foreignKey);
1078
+ const sourceColumn = resolveForeignKeyColumn(columns, foreignKey);
1079
+ const inverseLine = buildInverseRelationLine(sourceModelName, targetModel.modelName, foreignKey, columns);
1070
1080
  const targetBodyLines = targetModel.block.split("\n");
1071
- const fieldName = deriveCollectionFieldName(sourceModelName);
1081
+ const fieldName = isOneToOneForeignKey(sourceColumn) ? deriveSingularFieldName(sourceModelName) : deriveCollectionFieldName(sourceModelName);
1072
1082
  const fieldRegex = new RegExp(`^\\s*${escapeRegex(fieldName)}\\s+`);
1073
1083
  injectLineIntoModelBody(targetBodyLines, inverseLine, (line) => fieldRegex.test(line));
1074
1084
  const updatedTarget = targetBodyLines.join("\n");
@@ -1087,7 +1097,7 @@ const buildModelBlock = (operation) => {
1087
1097
  const modelName = toModelName(operation.table);
1088
1098
  const mapped = operation.table !== modelName.toLowerCase();
1089
1099
  const fields = operation.columns.map(buildFieldLine);
1090
- const relations = (operation.foreignKeys ?? []).map(buildRelationLine);
1100
+ const relations = (operation.foreignKeys ?? []).map((foreignKey) => buildRelationLine(modelName, foreignKey, operation.columns));
1091
1101
  const metadata = [...(operation.indexes ?? []).map(buildIndexLine), ...mapped ? [` @@map("${str(operation.table).snake()}")`] : []];
1092
1102
  return `model ${modelName} {\n${(metadata.length > 0 ? [
1093
1103
  ...fields,
@@ -1145,7 +1155,7 @@ const applyCreateTableOperation = (schema, operation) => {
1145
1155
  if (findModelBlock(schema, operation.table)) throw new ArkormException(`Prisma model for table [${operation.table}] already exists.`);
1146
1156
  const schemaWithEnums = ensureEnumBlocks(schema, operation.columns);
1147
1157
  const block = buildModelBlock(operation);
1148
- return applyInverseRelations(`${schemaWithEnums.trimEnd()}\n\n${block}\n`, toModelName(operation.table), operation.foreignKeys ?? []);
1158
+ return applyInverseRelations(`${schemaWithEnums.trimEnd()}\n\n${block}\n`, toModelName(operation.table), operation.foreignKeys ?? [], operation.columns);
1149
1159
  };
1150
1160
  /**
1151
1161
  * Apply an alter table operation to a Prisma schema string, modifying the model
@@ -1186,12 +1196,12 @@ const applyAlterTableOperation = (schema, operation) => {
1186
1196
  bodyLines.splice(insertIndex, 0, indexLine);
1187
1197
  });
1188
1198
  for (const foreignKey of operation.addForeignKeys ?? []) {
1189
- const relationLine = buildRelationLine(foreignKey);
1199
+ const relationLine = buildRelationLine(model.modelName, foreignKey, operation.addColumns);
1190
1200
  const relationRegex = new RegExp(`^\\s*${escapeRegex(foreignKey.fieldAlias?.trim() || deriveRelationFieldName(foreignKey.column))}\\s+`);
1191
1201
  injectLineIntoModelBody(bodyLines, relationLine, (line) => relationRegex.test(line));
1192
1202
  }
1193
1203
  block = bodyLines.join("\n");
1194
- return applyInverseRelations(`${schemaWithEnums.slice(0, refreshedModel.start)}${block}${schemaWithEnums.slice(refreshedModel.end)}`, model.modelName, operation.addForeignKeys ?? []);
1204
+ return applyInverseRelations(`${schemaWithEnums.slice(0, refreshedModel.start)}${block}${schemaWithEnums.slice(refreshedModel.end)}`, model.modelName, operation.addForeignKeys ?? [], operation.addColumns);
1195
1205
  };
1196
1206
  /**
1197
1207
  * Apply a drop table operation to a Prisma schema string, removing the model block
package/dist/index.cjs CHANGED
@@ -658,7 +658,7 @@ var TableBuilder = class {
658
658
  */
659
659
  foreign(column) {
660
660
  const columnName = this.resolveColumn(column).name;
661
- return this.foreignKey(columnName + (column ? "" : "Id"));
661
+ return this.foreignKey(column ?? columnName);
662
662
  }
663
663
  /**
664
664
  * Returns a deep copy of the defined columns for the table.
@@ -1076,24 +1076,24 @@ const deriveRelationFieldName = (columnName) => {
1076
1076
  if (trimmed.endsWith("_id") && trimmed.length > 3) return trimmed.slice(0, -3).replace(/_([a-zA-Z0-9])/g, (_, letter) => letter.toUpperCase());
1077
1077
  return `${trimmed.charAt(0).toLowerCase()}${trimmed.slice(1)}`;
1078
1078
  };
1079
- const pascalWords = (value) => {
1080
- return value.match(/[A-Z][a-z0-9]*/g) ?? [value];
1081
- };
1082
1079
  /**
1083
- * Derive a relation name for the inverse side of a relation based on the
1080
+ * Derive a relation name for both sides of a relation based on the
1084
1081
  * source and target model names, using an explicit alias if provided or a
1085
- * convention of combining the target model name with the last segment of
1086
- * the source model name.
1082
+ * convention of combining the full source model name with the target model name.
1087
1083
  *
1088
1084
  * @param sourceModelName The name of the source model in the relation.
1089
1085
  * @param targetModelName The name of the target model in the relation.
1090
- * @param explicitAlias An optional explicit alias for the inverse relation.
1091
- * @returns The derived or explicit inverse relation alias.
1086
+ * @param explicitAlias An optional explicit alias for the relation.
1087
+ * @returns The derived or explicit relation alias.
1092
1088
  */
1093
- const deriveInverseRelationAlias = (sourceModelName, targetModelName, explicitAlias) => {
1089
+ const deriveRelationAlias = (sourceModelName, targetModelName, explicitAlias) => {
1094
1090
  if (explicitAlias && explicitAlias.trim().length > 0) return explicitAlias.trim();
1095
- const sourceWords = pascalWords(sourceModelName);
1096
- return `${sourceWords[sourceWords.length - 1] ?? sourceModelName}${targetModelName}`;
1091
+ return [sourceModelName, targetModelName].sort((left, right) => left.localeCompare(right)).join("");
1092
+ };
1093
+ const deriveInverseRelationAlias = deriveRelationAlias;
1094
+ const deriveSingularFieldName = (modelName) => {
1095
+ if (!modelName) return "item";
1096
+ return `${modelName.charAt(0).toLowerCase()}${modelName.slice(1)}`;
1097
1097
  };
1098
1098
  const deriveCollectionFieldName = (modelName) => {
1099
1099
  if (!modelName) return "items";
@@ -1101,6 +1101,12 @@ const deriveCollectionFieldName = (modelName) => {
1101
1101
  if (camel.endsWith("s")) return `${camel}es`;
1102
1102
  return `${camel}s`;
1103
1103
  };
1104
+ const resolveForeignKeyColumn = (columns, foreignKey) => {
1105
+ return columns.find((column) => column.name === foreignKey.column);
1106
+ };
1107
+ const isOneToOneForeignKey = (column) => {
1108
+ return Boolean(column?.unique || column?.primary);
1109
+ };
1104
1110
  /**
1105
1111
  * Format a SchemaForeignKeyAction value as a Prisma onDelete action string.
1106
1112
  *
@@ -1121,15 +1127,16 @@ const formatRelationAction = (action) => {
1121
1127
  * @param foreignKey The foreign key definition to convert to a relation line.
1122
1128
  * @returns The corresponding Prisma schema line for the relation field.
1123
1129
  */
1124
- const buildRelationLine = (foreignKey) => {
1130
+ const buildRelationLine = (sourceModelName, foreignKey, columns = []) => {
1125
1131
  if (!foreignKey.referencesTable.trim()) throw new ArkormException(`Foreign key [${foreignKey.column}] must define a referenced table.`);
1126
1132
  if (!foreignKey.referencesColumn.trim()) throw new ArkormException(`Foreign key [${foreignKey.column}] must define a referenced column.`);
1133
+ const sourceColumn = resolveForeignKeyColumn(columns, foreignKey);
1127
1134
  const fieldName = foreignKey.fieldAlias?.trim() || deriveRelationFieldName(foreignKey.column);
1128
1135
  const targetModel = toModelName(foreignKey.referencesTable);
1129
- const relationName = foreignKey.relationAlias?.trim();
1130
- const relationPrefix = relationName ? `@relation("${relationName.replace(/"/g, "\\\"")}", ` : "@relation(";
1136
+ const relationName = deriveRelationAlias(sourceModelName, targetModel, foreignKey.relationAlias?.trim());
1137
+ const optional = sourceColumn?.nullable ? "?" : "";
1131
1138
  const onDelete = foreignKey.onDelete ? `, onDelete: ${formatRelationAction(foreignKey.onDelete)}` : "";
1132
- return ` ${fieldName} ${targetModel} ${relationPrefix}fields: [${foreignKey.column}], references: [${foreignKey.referencesColumn}]${onDelete})`;
1139
+ return ` ${fieldName} ${targetModel}${optional} @relation("${relationName.replace(/"/g, "\\\"")}", fields: [${foreignKey.column}], references: [${foreignKey.referencesColumn}]${onDelete})`;
1133
1140
  };
1134
1141
  /**
1135
1142
  * Build a Prisma relation field line for the inverse side of a relation, based
@@ -1141,8 +1148,11 @@ const buildRelationLine = (foreignKey) => {
1141
1148
  * @param foreignKey The foreign key definition for the relation.
1142
1149
  * @returns The Prisma schema line for the inverse relation field.
1143
1150
  */
1144
- const buildInverseRelationLine = (sourceModelName, targetModelName, foreignKey) => {
1145
- return ` ${deriveCollectionFieldName(sourceModelName)} ${sourceModelName}[] @relation("${deriveInverseRelationAlias(sourceModelName, targetModelName, foreignKey.inverseRelationAlias).replace(/"/g, "\\\"")}")`;
1151
+ const buildInverseRelationLine = (sourceModelName, targetModelName, foreignKey, columns = []) => {
1152
+ const sourceColumn = resolveForeignKeyColumn(columns, foreignKey);
1153
+ const fieldName = isOneToOneForeignKey(sourceColumn) ? deriveSingularFieldName(sourceModelName) : deriveCollectionFieldName(sourceModelName);
1154
+ const relationName = deriveRelationAlias(sourceModelName, targetModelName, foreignKey.relationAlias?.trim());
1155
+ return ` ${fieldName} ${isOneToOneForeignKey(sourceColumn) ? `${sourceModelName}?` : `${sourceModelName}[]`} @relation("${relationName.replace(/"/g, "\\\"")}")`;
1146
1156
  };
1147
1157
  /**
1148
1158
  * Inject a line into the body of a Prisma model block if it does not already
@@ -1170,14 +1180,15 @@ const injectLineIntoModelBody = (bodyLines, line, exists) => {
1170
1180
  * @param foreignKeys An array of foreign key definitions to process.
1171
1181
  * @returns The updated Prisma schema string with inverse relations applied.
1172
1182
  */
1173
- const applyInverseRelations = (schema, sourceModelName, foreignKeys) => {
1183
+ const applyInverseRelations = (schema, sourceModelName, foreignKeys, columns = []) => {
1174
1184
  let nextSchema = schema;
1175
1185
  for (const foreignKey of foreignKeys) {
1176
1186
  const targetModel = findModelBlock(nextSchema, foreignKey.referencesTable);
1177
1187
  if (!targetModel) continue;
1178
- const inverseLine = buildInverseRelationLine(sourceModelName, targetModel.modelName, foreignKey);
1188
+ const sourceColumn = resolveForeignKeyColumn(columns, foreignKey);
1189
+ const inverseLine = buildInverseRelationLine(sourceModelName, targetModel.modelName, foreignKey, columns);
1179
1190
  const targetBodyLines = targetModel.block.split("\n");
1180
- const fieldName = deriveCollectionFieldName(sourceModelName);
1191
+ const fieldName = isOneToOneForeignKey(sourceColumn) ? deriveSingularFieldName(sourceModelName) : deriveCollectionFieldName(sourceModelName);
1181
1192
  const fieldRegex = new RegExp(`^\\s*${escapeRegex(fieldName)}\\s+`);
1182
1193
  injectLineIntoModelBody(targetBodyLines, inverseLine, (line) => fieldRegex.test(line));
1183
1194
  const updatedTarget = targetBodyLines.join("\n");
@@ -1196,7 +1207,7 @@ const buildModelBlock = (operation) => {
1196
1207
  const modelName = toModelName(operation.table);
1197
1208
  const mapped = operation.table !== modelName.toLowerCase();
1198
1209
  const fields = operation.columns.map(buildFieldLine);
1199
- const relations = (operation.foreignKeys ?? []).map(buildRelationLine);
1210
+ const relations = (operation.foreignKeys ?? []).map((foreignKey) => buildRelationLine(modelName, foreignKey, operation.columns));
1200
1211
  const metadata = [...(operation.indexes ?? []).map(buildIndexLine), ...mapped ? [` @@map("${(0, _h3ravel_support.str)(operation.table).snake()}")`] : []];
1201
1212
  return `model ${modelName} {\n${(metadata.length > 0 ? [
1202
1213
  ...fields,
@@ -1254,7 +1265,7 @@ const applyCreateTableOperation = (schema, operation) => {
1254
1265
  if (findModelBlock(schema, operation.table)) throw new ArkormException(`Prisma model for table [${operation.table}] already exists.`);
1255
1266
  const schemaWithEnums = ensureEnumBlocks(schema, operation.columns);
1256
1267
  const block = buildModelBlock(operation);
1257
- return applyInverseRelations(`${schemaWithEnums.trimEnd()}\n\n${block}\n`, toModelName(operation.table), operation.foreignKeys ?? []);
1268
+ return applyInverseRelations(`${schemaWithEnums.trimEnd()}\n\n${block}\n`, toModelName(operation.table), operation.foreignKeys ?? [], operation.columns);
1258
1269
  };
1259
1270
  /**
1260
1271
  * Apply an alter table operation to a Prisma schema string, modifying the model
@@ -1295,12 +1306,12 @@ const applyAlterTableOperation = (schema, operation) => {
1295
1306
  bodyLines.splice(insertIndex, 0, indexLine);
1296
1307
  });
1297
1308
  for (const foreignKey of operation.addForeignKeys ?? []) {
1298
- const relationLine = buildRelationLine(foreignKey);
1309
+ const relationLine = buildRelationLine(model.modelName, foreignKey, operation.addColumns);
1299
1310
  const relationRegex = new RegExp(`^\\s*${escapeRegex(foreignKey.fieldAlias?.trim() || deriveRelationFieldName(foreignKey.column))}\\s+`);
1300
1311
  injectLineIntoModelBody(bodyLines, relationLine, (line) => relationRegex.test(line));
1301
1312
  }
1302
1313
  block = bodyLines.join("\n");
1303
- return applyInverseRelations(`${schemaWithEnums.slice(0, refreshedModel.start)}${block}${schemaWithEnums.slice(refreshedModel.end)}`, model.modelName, operation.addForeignKeys ?? []);
1314
+ return applyInverseRelations(`${schemaWithEnums.slice(0, refreshedModel.start)}${block}${schemaWithEnums.slice(refreshedModel.end)}`, model.modelName, operation.addForeignKeys ?? [], operation.addColumns);
1304
1315
  };
1305
1316
  /**
1306
1317
  * Apply a drop table operation to a Prisma schema string, removing the model block
@@ -3438,6 +3449,30 @@ var Relation = class {
3438
3449
  if (results instanceof ArkormCollection) return results.all()[0] ?? null;
3439
3450
  return results;
3440
3451
  }
3452
+ /**
3453
+ * Count records that match the relationship query.
3454
+ *
3455
+ * @returns
3456
+ */
3457
+ async count() {
3458
+ return (await this.getQuery()).count();
3459
+ }
3460
+ /**
3461
+ * Determine whether the relationship query has any matching records.
3462
+ *
3463
+ * @returns
3464
+ */
3465
+ async exists() {
3466
+ return (await this.getQuery()).exists();
3467
+ }
3468
+ /**
3469
+ * Determine whether the relationship query has no matching records.
3470
+ *
3471
+ * @returns
3472
+ */
3473
+ async doesntExist() {
3474
+ return !await this.exists();
3475
+ }
3441
3476
  };
3442
3477
 
3443
3478
  //#endregion
@@ -3460,15 +3495,55 @@ var BelongsToManyRelation = class extends Relation {
3460
3495
  this.relatedKey = relatedKey;
3461
3496
  }
3462
3497
  /**
3498
+ * Build the relationship query.
3499
+ *
3500
+ * @returns
3501
+ */
3502
+ async getQuery() {
3503
+ const parentValue = this.parent.getAttribute(this.parentKey);
3504
+ const ids = (await this.related.getDelegate(this.throughDelegate).findMany({ where: { [this.foreignPivotKey]: parentValue } })).map((row) => row[this.relatedPivotKey]);
3505
+ return this.applyConstraint(this.related.query().where({ [this.relatedKey]: { in: ids } }));
3506
+ }
3507
+ /**
3463
3508
  * Fetches the related models for this relationship.
3464
3509
  *
3465
3510
  * @returns
3466
3511
  */
3467
3512
  async getResults() {
3468
- const parentValue = this.parent.getAttribute(this.parentKey);
3469
- const ids = (await this.related.getDelegate(this.throughDelegate).findMany({ where: { [this.foreignPivotKey]: parentValue } })).map((row) => row[this.relatedPivotKey]);
3470
- if (ids.length === 0) return new ArkormCollection([]);
3471
- return this.applyConstraint(this.related.query().where({ [this.relatedKey]: { in: ids } })).get();
3513
+ return (await this.getQuery()).get();
3514
+ }
3515
+ };
3516
+
3517
+ //#endregion
3518
+ //#region src/relationship/SingleResultRelation.ts
3519
+ /**
3520
+ * Base class for relationships that resolve to a single related model.
3521
+ *
3522
+ * @author Legacy (3m1n3nc3)
3523
+ * @since 1.3.0
3524
+ */
3525
+ var SingleResultRelation = class extends Relation {
3526
+ defaultValue;
3527
+ constructor(parent, related) {
3528
+ super();
3529
+ this.parent = parent;
3530
+ this.related = related;
3531
+ }
3532
+ /**
3533
+ * Defines a default value to return when the relationship does not find a related model.
3534
+ *
3535
+ * @param value The default value or a callback that returns the default value.
3536
+ * @returns The current instance for method chaining.
3537
+ */
3538
+ withDefault(value = {}) {
3539
+ this.defaultValue = value;
3540
+ return this;
3541
+ }
3542
+ resolveDefaultResult() {
3543
+ if (typeof this.defaultValue === "undefined") return null;
3544
+ const resolved = typeof this.defaultValue === "function" ? this.defaultValue(this.parent) : this.defaultValue;
3545
+ if (resolved instanceof this.related) return resolved;
3546
+ return this.related.hydrate(resolved);
3472
3547
  }
3473
3548
  };
3474
3549
 
@@ -3480,22 +3555,28 @@ var BelongsToManyRelation = class extends Relation {
3480
3555
  * @author Legacy (3m1n3nc3)
3481
3556
  * @since 0.1.0
3482
3557
  */
3483
- var BelongsToRelation = class extends Relation {
3558
+ var BelongsToRelation = class extends SingleResultRelation {
3484
3559
  constructor(parent, related, foreignKey, ownerKey) {
3485
- super();
3486
- this.parent = parent;
3487
- this.related = related;
3560
+ super(parent, related);
3488
3561
  this.foreignKey = foreignKey;
3489
3562
  this.ownerKey = ownerKey;
3490
3563
  }
3491
3564
  /**
3565
+ * Build the relationship query.
3566
+ *
3567
+ * @returns
3568
+ */
3569
+ async getQuery() {
3570
+ const foreignValue = this.parent.getAttribute(this.foreignKey);
3571
+ return this.applyConstraint(this.related.query().where({ [this.ownerKey]: foreignValue }));
3572
+ }
3573
+ /**
3492
3574
  * Fetches the related models for this relationship.
3493
3575
  *
3494
3576
  * @returns
3495
3577
  */
3496
3578
  async getResults() {
3497
- const foreignValue = this.parent.getAttribute(this.foreignKey);
3498
- return this.applyConstraint(this.related.query().where({ [this.ownerKey]: foreignValue })).first();
3579
+ return await (await this.getQuery()).first() ?? this.resolveDefaultResult();
3499
3580
  }
3500
3581
  };
3501
3582
 
@@ -3516,13 +3597,21 @@ var HasManyRelation = class extends Relation {
3516
3597
  this.localKey = localKey;
3517
3598
  }
3518
3599
  /**
3600
+ * Build the relationship query.
3601
+ *
3602
+ * @returns
3603
+ */
3604
+ async getQuery() {
3605
+ const localValue = this.parent.getAttribute(this.localKey);
3606
+ return this.applyConstraint(this.related.query().where({ [this.foreignKey]: localValue }));
3607
+ }
3608
+ /**
3519
3609
  * Fetches the related models for this relationship.
3520
3610
  *
3521
3611
  * @returns
3522
3612
  */
3523
3613
  async getResults() {
3524
- const localValue = this.parent.getAttribute(this.localKey);
3525
- return this.applyConstraint(this.related.query().where({ [this.foreignKey]: localValue })).get();
3614
+ return (await this.getQuery()).get();
3526
3615
  }
3527
3616
  };
3528
3617
 
@@ -3547,15 +3636,22 @@ var HasManyThroughRelation = class extends Relation {
3547
3636
  this.secondLocalKey = secondLocalKey;
3548
3637
  }
3549
3638
  /**
3639
+ * Build the relationship query.
3640
+ *
3641
+ * @returns
3642
+ */
3643
+ async getQuery() {
3644
+ const localValue = this.parent.getAttribute(this.localKey);
3645
+ const keys = (await this.related.getDelegate(this.throughDelegate).findMany({ where: { [this.firstKey]: localValue } })).map((row) => row[this.secondLocalKey]);
3646
+ return this.applyConstraint(this.related.query().where({ [this.secondKey]: { in: keys } }));
3647
+ }
3648
+ /**
3550
3649
  * Fetches the related models for this relationship.
3551
3650
  *
3552
3651
  * @returns
3553
3652
  */
3554
3653
  async getResults() {
3555
- const localValue = this.parent.getAttribute(this.localKey);
3556
- const keys = (await this.related.getDelegate(this.throughDelegate).findMany({ where: { [this.firstKey]: localValue } })).map((row) => row[this.secondLocalKey]);
3557
- if (keys.length === 0) return new ArkormCollection([]);
3558
- return this.applyConstraint(this.related.query().where({ [this.secondKey]: { in: keys } })).get();
3654
+ return (await this.getQuery()).get();
3559
3655
  }
3560
3656
  };
3561
3657
 
@@ -3567,22 +3663,28 @@ var HasManyThroughRelation = class extends Relation {
3567
3663
  * @author Legacy (3m1n3nc3)
3568
3664
  * @since 0.1.0
3569
3665
  */
3570
- var HasOneRelation = class extends Relation {
3666
+ var HasOneRelation = class extends SingleResultRelation {
3571
3667
  constructor(parent, related, foreignKey, localKey) {
3572
- super();
3573
- this.parent = parent;
3574
- this.related = related;
3668
+ super(parent, related);
3575
3669
  this.foreignKey = foreignKey;
3576
3670
  this.localKey = localKey;
3577
3671
  }
3578
3672
  /**
3673
+ * Build the relationship query.
3674
+ *
3675
+ * @returns
3676
+ */
3677
+ async getQuery() {
3678
+ const localValue = this.parent.getAttribute(this.localKey);
3679
+ return this.applyConstraint(this.related.query().where({ [this.foreignKey]: localValue }));
3680
+ }
3681
+ /**
3579
3682
  * Fetches the related models for this relationship.
3580
3683
  *
3581
3684
  * @returns
3582
3685
  */
3583
3686
  async getResults() {
3584
- const localValue = this.parent.getAttribute(this.localKey);
3585
- return this.applyConstraint(this.related.query().where({ [this.foreignKey]: localValue })).first();
3687
+ return await (await this.getQuery()).first() ?? this.resolveDefaultResult();
3586
3688
  }
3587
3689
  };
3588
3690
 
@@ -3595,11 +3697,9 @@ var HasOneRelation = class extends Relation {
3595
3697
  * @author Legacy (3m1n3nc3)
3596
3698
  * @since 0.1.0
3597
3699
  */
3598
- var HasOneThroughRelation = class extends Relation {
3700
+ var HasOneThroughRelation = class extends SingleResultRelation {
3599
3701
  constructor(parent, related, throughDelegate, firstKey, secondKey, localKey, secondLocalKey) {
3600
- super();
3601
- this.parent = parent;
3602
- this.related = related;
3702
+ super(parent, related);
3603
3703
  this.throughDelegate = throughDelegate;
3604
3704
  this.firstKey = firstKey;
3605
3705
  this.secondKey = secondKey;
@@ -3607,15 +3707,23 @@ var HasOneThroughRelation = class extends Relation {
3607
3707
  this.secondLocalKey = secondLocalKey;
3608
3708
  }
3609
3709
  /**
3710
+ * Build the relationship query.
3711
+ *
3712
+ * @returns
3713
+ */
3714
+ async getQuery() {
3715
+ const localValue = this.parent.getAttribute(this.localKey);
3716
+ const intermediate = await this.related.getDelegate(this.throughDelegate).findFirst({ where: { [this.firstKey]: localValue } });
3717
+ if (!intermediate) return this.applyConstraint(this.related.query().where({ [this.secondKey]: { in: [] } }));
3718
+ return this.applyConstraint(this.related.query().where({ [this.secondKey]: intermediate[this.secondLocalKey] }));
3719
+ }
3720
+ /**
3610
3721
  * Fetches the related models for this relationship.
3611
3722
  *
3612
3723
  * @returns
3613
3724
  */
3614
3725
  async getResults() {
3615
- const localValue = this.parent.getAttribute(this.localKey);
3616
- const intermediate = await this.related.getDelegate(this.throughDelegate).findFirst({ where: { [this.firstKey]: localValue } });
3617
- if (!intermediate) return null;
3618
- return this.applyConstraint(this.related.query().where({ [this.secondKey]: intermediate[this.secondLocalKey] })).first();
3726
+ return await (await this.getQuery()).first() ?? this.resolveDefaultResult();
3619
3727
  }
3620
3728
  };
3621
3729
 
@@ -3636,17 +3744,25 @@ var MorphManyRelation = class extends Relation {
3636
3744
  this.localKey = localKey;
3637
3745
  }
3638
3746
  /**
3639
- * Fetches the related models for this relationship.
3640
- *
3641
- * @returns
3747
+ * Build the relationship query.
3748
+ *
3749
+ * @returns
3642
3750
  */
3643
- async getResults() {
3751
+ async getQuery() {
3644
3752
  const id = this.parent.getAttribute(this.localKey);
3645
3753
  const type = this.parent.constructor.name;
3646
3754
  return this.applyConstraint(this.related.query().where({
3647
3755
  [`${this.morphName}Id`]: id,
3648
3756
  [`${this.morphName}Type`]: type
3649
- })).get();
3757
+ }));
3758
+ }
3759
+ /**
3760
+ * Fetches the related models for this relationship.
3761
+ *
3762
+ * @returns
3763
+ */
3764
+ async getResults() {
3765
+ return (await this.getQuery()).get();
3650
3766
  }
3651
3767
  };
3652
3768
 
@@ -3658,26 +3774,32 @@ var MorphManyRelation = class extends Relation {
3658
3774
  * @author Legacy (3m1n3nc3)
3659
3775
  * @since 0.1.0
3660
3776
  */
3661
- var MorphOneRelation = class extends Relation {
3777
+ var MorphOneRelation = class extends SingleResultRelation {
3662
3778
  constructor(parent, related, morphName, localKey) {
3663
- super();
3664
- this.parent = parent;
3665
- this.related = related;
3779
+ super(parent, related);
3666
3780
  this.morphName = morphName;
3667
3781
  this.localKey = localKey;
3668
3782
  }
3669
3783
  /**
3670
- * Fetches the related models for this relationship.
3671
- *
3672
- * @returns
3784
+ * Build the relationship query.
3785
+ *
3786
+ * @returns
3673
3787
  */
3674
- async getResults() {
3788
+ async getQuery() {
3675
3789
  const id = this.parent.getAttribute(this.localKey);
3676
3790
  const type = this.parent.constructor.name;
3677
3791
  return this.applyConstraint(this.related.query().where({
3678
3792
  [`${this.morphName}Id`]: id,
3679
3793
  [`${this.morphName}Type`]: type
3680
- })).first();
3794
+ }));
3795
+ }
3796
+ /**
3797
+ * Fetches the related models for this relationship.
3798
+ *
3799
+ * @returns
3800
+ */
3801
+ async getResults() {
3802
+ return await (await this.getQuery()).first() ?? this.resolveDefaultResult();
3681
3803
  }
3682
3804
  };
3683
3805
 
@@ -3701,19 +3823,26 @@ var MorphToManyRelation = class extends Relation {
3701
3823
  this.relatedKey = relatedKey;
3702
3824
  }
3703
3825
  /**
3704
- * Fetches the related models for this relationship.
3705
- *
3706
- * @returns
3826
+ * Build the relationship query.
3827
+ *
3828
+ * @returns
3707
3829
  */
3708
- async getResults() {
3830
+ async getQuery() {
3709
3831
  const parentValue = this.parent.getAttribute(this.parentKey);
3710
3832
  const morphType = this.parent.constructor.name;
3711
3833
  const ids = (await this.related.getDelegate(this.throughDelegate).findMany({ where: {
3712
3834
  [`${this.morphName}Id`]: parentValue,
3713
3835
  [`${this.morphName}Type`]: morphType
3714
3836
  } })).map((row) => row[this.relatedPivotKey]);
3715
- if (ids.length === 0) return new ArkormCollection([]);
3716
- return this.applyConstraint(this.related.query().where({ [this.relatedKey]: { in: ids } })).get();
3837
+ return this.applyConstraint(this.related.query().where({ [this.relatedKey]: { in: ids } }));
3838
+ }
3839
+ /**
3840
+ * Fetches the related models for this relationship.
3841
+ *
3842
+ * @returns
3843
+ */
3844
+ async getResults() {
3845
+ return (await this.getQuery()).get();
3717
3846
  }
3718
3847
  };
3719
3848
 
@@ -6468,7 +6597,9 @@ exports.defineConfig = defineConfig;
6468
6597
  exports.defineFactory = defineFactory;
6469
6598
  exports.deriveCollectionFieldName = deriveCollectionFieldName;
6470
6599
  exports.deriveInverseRelationAlias = deriveInverseRelationAlias;
6600
+ exports.deriveRelationAlias = deriveRelationAlias;
6471
6601
  exports.deriveRelationFieldName = deriveRelationFieldName;
6602
+ exports.deriveSingularFieldName = deriveSingularFieldName;
6472
6603
  exports.ensureArkormConfigLoading = ensureArkormConfigLoading;
6473
6604
  exports.escapeRegex = escapeRegex;
6474
6605
  exports.findAppliedMigration = findAppliedMigration;