@rvoh/dream 2.6.1-alpha.1 → 2.7.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 (202) hide show
  1. package/dist/cjs/src/bin/index.js +2 -0
  2. package/dist/cjs/src/cli/index.js +1 -0
  3. package/dist/cjs/src/helpers/cli/generateDream.js +7 -1
  4. package/dist/cjs/src/helpers/cli/generateDreamContent.js +44 -9
  5. package/dist/cjs/src/helpers/cli/generateMigration.js +2 -1
  6. package/dist/cjs/src/helpers/cli/generateMigrationContent.js +28 -11
  7. package/dist/cjs/src/ops/index.js +245 -1
  8. package/dist/esm/src/bin/index.js +2 -0
  9. package/dist/esm/src/cli/index.js +1 -0
  10. package/dist/esm/src/helpers/cli/generateDream.js +7 -1
  11. package/dist/esm/src/helpers/cli/generateDreamContent.js +44 -9
  12. package/dist/esm/src/helpers/cli/generateMigration.js +2 -1
  13. package/dist/esm/src/helpers/cli/generateMigrationContent.js +28 -11
  14. package/dist/esm/src/ops/index.js +245 -1
  15. package/dist/types/src/bin/index.d.ts +1 -0
  16. package/dist/types/src/cli/index.d.ts +7 -0
  17. package/dist/types/src/helpers/cli/generateDream.d.ts +8 -0
  18. package/dist/types/src/helpers/cli/generateDreamContent.d.ts +11 -1
  19. package/dist/types/src/helpers/cli/generateMigration.d.ts +7 -1
  20. package/dist/types/src/helpers/cli/generateMigrationContent.d.ts +7 -1
  21. package/dist/types/src/ops/index.d.ts +245 -0
  22. package/dist/types/src/types/associations/shared.d.ts +2 -2
  23. package/dist/types/src/types/associations/shared.ts +17 -11
  24. package/docs/assets/search.js +1 -1
  25. package/docs/classes/db.DreamMigrationHelpers.html +9 -9
  26. package/docs/classes/db.KyselyQueryDriver.html +32 -32
  27. package/docs/classes/db.PostgresQueryDriver.html +33 -33
  28. package/docs/classes/db.QueryDriverBase.html +31 -31
  29. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  30. package/docs/classes/errors.ColumnOverflow.html +3 -3
  31. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  32. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  33. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  34. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  35. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  36. package/docs/classes/errors.InvalidClockTime.html +2 -2
  37. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  38. package/docs/classes/errors.InvalidDateTime.html +2 -2
  39. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  40. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  41. package/docs/classes/errors.NotNullViolation.html +3 -3
  42. package/docs/classes/errors.RecordNotFound.html +3 -3
  43. package/docs/classes/errors.ValidationError.html +3 -3
  44. package/docs/classes/index.CalendarDate.html +33 -33
  45. package/docs/classes/index.ClockTime.html +32 -32
  46. package/docs/classes/index.ClockTimeTz.html +35 -35
  47. package/docs/classes/index.DateTime.html +86 -86
  48. package/docs/classes/index.Decorators.html +19 -19
  49. package/docs/classes/index.Dream.html +119 -119
  50. package/docs/classes/index.DreamApp.html +5 -5
  51. package/docs/classes/index.DreamTransaction.html +2 -2
  52. package/docs/classes/index.Env.html +2 -2
  53. package/docs/classes/index.Query.html +56 -56
  54. package/docs/classes/system.CliFileWriter.html +4 -4
  55. package/docs/classes/system.DreamBin.html +2 -2
  56. package/docs/classes/system.DreamCLI.html +6 -6
  57. package/docs/classes/system.DreamImporter.html +2 -2
  58. package/docs/classes/system.DreamLogos.html +2 -2
  59. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  60. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  61. package/docs/classes/system.PathHelpers.html +3 -3
  62. package/docs/classes/utils.Encrypt.html +2 -2
  63. package/docs/classes/utils.Range.html +2 -2
  64. package/docs/functions/db.closeAllDbConnections.html +1 -1
  65. package/docs/functions/db.dreamDbConnections.html +1 -1
  66. package/docs/functions/db.untypedDb.html +1 -1
  67. package/docs/functions/db.validateColumn.html +1 -1
  68. package/docs/functions/db.validateTable.html +1 -1
  69. package/docs/functions/errors.pgErrorType.html +1 -1
  70. package/docs/functions/index.DreamSerializer.html +1 -1
  71. package/docs/functions/index.ObjectSerializer.html +1 -1
  72. package/docs/functions/index.ReplicaSafe.html +1 -1
  73. package/docs/functions/index.STI.html +1 -1
  74. package/docs/functions/index.SoftDelete.html +1 -1
  75. package/docs/functions/utils.camelize.html +1 -1
  76. package/docs/functions/utils.capitalize.html +1 -1
  77. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  78. package/docs/functions/utils.compact.html +1 -1
  79. package/docs/functions/utils.groupBy.html +1 -1
  80. package/docs/functions/utils.hyphenize.html +1 -1
  81. package/docs/functions/utils.intersection.html +1 -1
  82. package/docs/functions/utils.isEmpty.html +1 -1
  83. package/docs/functions/utils.normalizeUnicode.html +1 -1
  84. package/docs/functions/utils.pascalize.html +1 -1
  85. package/docs/functions/utils.percent.html +1 -1
  86. package/docs/functions/utils.range.html +1 -1
  87. package/docs/functions/utils.round.html +1 -1
  88. package/docs/functions/utils.sanitizeString.html +1 -1
  89. package/docs/functions/utils.snakeify.html +1 -1
  90. package/docs/functions/utils.sort.html +1 -1
  91. package/docs/functions/utils.sortBy.html +1 -1
  92. package/docs/functions/utils.sortObjectByKey.html +1 -1
  93. package/docs/functions/utils.sortObjectByValue.html +1 -1
  94. package/docs/functions/utils.uncapitalize.html +1 -1
  95. package/docs/functions/utils.uniq.html +1 -1
  96. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  97. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  98. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  99. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  100. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  101. package/docs/interfaces/types.DecoratorContext.html +2 -2
  102. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  103. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  104. package/docs/interfaces/types.DurationObject.html +2 -2
  105. package/docs/interfaces/types.EncryptOptions.html +2 -2
  106. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  107. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  108. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  109. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  110. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  111. package/docs/types/openapi.OpenapiFormats.html +1 -1
  112. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  113. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  114. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  115. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  116. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  117. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  118. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  119. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  120. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  121. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  122. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  123. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  124. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  125. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  126. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  127. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  128. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  129. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  130. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  131. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  132. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  133. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  134. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  135. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  136. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  137. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  138. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  139. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  140. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  141. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  142. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  143. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  144. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  145. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  146. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  147. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  148. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  149. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  150. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  151. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  152. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  153. package/docs/types/types.CalendarDateObject.html +1 -1
  154. package/docs/types/types.Camelized.html +1 -1
  155. package/docs/types/types.ClockTimeObject.html +1 -1
  156. package/docs/types/types.DbConnectionType.html +1 -1
  157. package/docs/types/types.DbTypes.html +1 -1
  158. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  159. package/docs/types/types.DreamAttributes.html +1 -1
  160. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  161. package/docs/types/types.DreamClassColumn.html +1 -1
  162. package/docs/types/types.DreamColumn.html +1 -1
  163. package/docs/types/types.DreamColumnNames.html +1 -1
  164. package/docs/types/types.DreamLogLevel.html +1 -1
  165. package/docs/types/types.DreamLogger.html +2 -2
  166. package/docs/types/types.DreamModelSerializerType.html +1 -1
  167. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  168. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  169. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  170. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  171. package/docs/types/types.DreamSerializable.html +1 -1
  172. package/docs/types/types.DreamSerializableArray.html +1 -1
  173. package/docs/types/types.DreamSerializerKey.html +1 -1
  174. package/docs/types/types.DreamSerializers.html +1 -1
  175. package/docs/types/types.DreamVirtualColumns.html +1 -1
  176. package/docs/types/types.DurationUnit.html +1 -1
  177. package/docs/types/types.EncryptAlgorithm.html +1 -1
  178. package/docs/types/types.HasManyStatement.html +1 -1
  179. package/docs/types/types.HasOneStatement.html +1 -1
  180. package/docs/types/types.Hyphenized.html +1 -1
  181. package/docs/types/types.Pascalized.html +1 -1
  182. package/docs/types/types.PrimaryKeyType.html +1 -1
  183. package/docs/types/types.RoundingPrecision.html +1 -1
  184. package/docs/types/types.SerializerCasing.html +1 -1
  185. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  186. package/docs/types/types.Snakeified.html +1 -1
  187. package/docs/types/types.StrictInterface.html +1 -1
  188. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  189. package/docs/types/types.UpdateableProperties.html +1 -1
  190. package/docs/types/types.ValidationType.html +1 -1
  191. package/docs/types/types.ViewModel.html +2 -2
  192. package/docs/types/types.ViewModelClass.html +1 -1
  193. package/docs/types/types.WeekdayName.html +1 -1
  194. package/docs/types/types.WhereStatementForDream.html +1 -1
  195. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  196. package/docs/variables/index.DreamConst.html +1 -1
  197. package/docs/variables/index.ops.html +115 -1
  198. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  199. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  200. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  201. package/docs/variables/system.primaryKeyTypes.html +1 -1
  202. package/package.json +1 -1
@@ -62,6 +62,8 @@ export default class DreamBin {
62
62
  includeInternalSerializers: options.internalSerializers ?? false,
63
63
  ...options,
64
64
  stiBaseSerializer: false,
65
+ // `@SoftDelete()` is incompatible with STI children — never auto-apply.
66
+ softDelete: false,
65
67
  },
66
68
  fullyQualifiedParentName,
67
69
  });
@@ -190,6 +190,7 @@ ${INDENT} pnpm psy g:model --model-name=GroupDanceLesson Lesson/Dance/Group
190
190
  ${INDENT} # model is named GroupDanceLesson instead of LessonDanceGroup`)
191
191
  .option('--admin-serializers', 'also generate AdminSerializer and AdminSummarySerializer variants for admin-facing API endpoints that may expose additional fields', false)
192
192
  .option('--internal-serializers', 'also generate InternalSerializer and InternalSummarySerializer variants for internal API endpoints that may expose additional fields', false)
193
+ .option('--no-soft-delete', `skip generating the @SoftDelete() decorator and the corresponding nullable \`deleted_at\` column. By default, generated models use soft-delete semantics (rows are marked deleted via \`deleted_at\` instead of being removed from the database). Pass this flag when you want records to be hard-deleted.`)
193
194
  .argument('<modelName>', `The fully qualified model name, using / for namespacing. This determines the model class name (may be overridden with \`--model-name\`), table name, and file path under src/app/models/.
194
195
  ${INDENT}
195
196
  ${INDENT}Examples:
@@ -9,6 +9,11 @@ import writeGeneratedFile from './writeGeneratedFile.js';
9
9
  export default async function generateDream({ fullyQualifiedModelName, columnsWithTypes, options, fullyQualifiedParentName, }) {
10
10
  fullyQualifiedModelName = standardizeFullyQualifiedModelName(fullyQualifiedModelName);
11
11
  const modelClassName = modelClassNameFrom(fullyQualifiedModelName, options.modelName);
12
+ const isSTI = !!fullyQualifiedParentName;
13
+ // `@SoftDelete()` is incompatible with STI children, so we force-disable
14
+ // the soft-delete scaffold when generating an STI child regardless of the
15
+ // caller-provided value.
16
+ const softDelete = !isSTI && !!options.softDelete;
12
17
  await writeGeneratedFile({
13
18
  dreamPathKey: 'models',
14
19
  fileName: `${fullyQualifiedModelName}.ts`,
@@ -22,6 +27,7 @@ export default async function generateDream({ fullyQualifiedModelName, columnsWi
22
27
  connectionName: options.connectionName,
23
28
  tableName: options.tableName,
24
29
  modelClassName,
30
+ softDelete,
25
31
  }),
26
32
  logLabel: 'dream',
27
33
  });
@@ -37,7 +43,6 @@ export default async function generateDream({ fullyQualifiedModelName, columnsWi
37
43
  includeInternalSerializers: options.includeInternalSerializers ?? false,
38
44
  modelClassName,
39
45
  });
40
- const isSTI = !!fullyQualifiedParentName;
41
46
  if (columnsWithTypes.length || !isSTI) {
42
47
  await generateMigration({
43
48
  connectionName: options.connectionName,
@@ -47,6 +52,7 @@ export default async function generateDream({ fullyQualifiedModelName, columnsWi
47
52
  fullyQualifiedParentName,
48
53
  tableName: options.tableName,
49
54
  modelClassName,
55
+ softDelete,
50
56
  });
51
57
  }
52
58
  }
@@ -7,19 +7,47 @@ import absoluteDreamPath from '../path/absoluteDreamPath.js';
7
7
  import snakeify from '../snakeify.js';
8
8
  import standardizeFullyQualifiedModelName from '../standardizeFullyQualifiedModelName.js';
9
9
  import uniq from '../uniq.js';
10
+ /**
11
+ * Column names that are automatically emitted by the model generator (and
12
+ * the migration generator). When the user passes any of these explicitly,
13
+ * we filter them out of the attribute loop so the model class and migration
14
+ * don't end up with duplicate declarations.
15
+ */
16
+ const AUTO_GENERATED_TIMESTAMP_COLUMN_NAMES = ['created_at', 'updated_at', 'deleted_at'];
17
+ function columnName(columnWithType) {
18
+ return columnWithType.split(':')[0] ?? '';
19
+ }
20
+ export function hasExplicitColumn(columnsWithTypes, name) {
21
+ return columnsWithTypes.some(col => columnName(col) === name);
22
+ }
23
+ export function filterAutoGeneratedTimestampColumns(columnsWithTypes) {
24
+ return columnsWithTypes.filter(col => !AUTO_GENERATED_TIMESTAMP_COLUMN_NAMES.includes(columnName(col)));
25
+ }
10
26
  export default function generateDreamContent(options) {
11
27
  const config = createModelConfig(options);
12
- const baseImports = createImportConfig(config, options);
13
- const attributesResult = processAttributes(options.columnsWithTypes, config.modelClassName);
28
+ // SoftDelete is incompatible with STI children, so it's only applied to
29
+ // non-STI-child models.
30
+ const includeSoftDelete = !config.isSTI && !!options.softDelete;
31
+ const hasExplicitDeletedAt = hasExplicitColumn(options.columnsWithTypes, 'deleted_at');
32
+ // Filter out timestamp columns that we auto-generate from the attribute
33
+ // processing loop so we don't double-generate them in the model class.
34
+ // We only dedupe on non-STI models — STI children don't get auto timestamps.
35
+ const columnsForAttributes = config.isSTI
36
+ ? options.columnsWithTypes
37
+ : filterAutoGeneratedTimestampColumns(options.columnsWithTypes);
38
+ const baseImports = createImportConfig(config, options, { includeSoftDelete });
39
+ const attributesResult = processAttributes(columnsForAttributes, config.modelClassName);
14
40
  const allImports = {
15
41
  ...baseImports,
16
42
  modelImportStatements: [...baseImports.modelImportStatements, ...attributesResult.imports],
17
43
  };
18
44
  const importSection = buildImportSection(allImports);
19
- const classDeclaration = buildClassDeclaration(config);
45
+ const classDeclaration = buildClassDeclaration(config, { includeSoftDelete });
20
46
  const tableMethod = buildTableMethod(config);
21
47
  const serializersMethod = buildSerializersMethod(config, options);
22
- const fieldsSection = buildFieldsSection(config, attributesResult);
48
+ const fieldsSection = buildFieldsSection(config, attributesResult, {
49
+ includeDeletedAt: includeSoftDelete || hasExplicitDeletedAt,
50
+ });
23
51
  return `${importSection}
24
52
 
25
53
  const deco = new Decorators<typeof ${config.modelClassName}>()
@@ -50,7 +78,7 @@ export function createModelConfig(options) {
50
78
  tableName,
51
79
  };
52
80
  }
53
- export function createImportConfig(config, options) {
81
+ export function createImportConfig(config, options, { includeSoftDelete }) {
54
82
  const dreamTypeImports = ['DreamColumn'];
55
83
  const dreamImports = ['Decorators'];
56
84
  if (options.serializer) {
@@ -59,6 +87,9 @@ export function createImportConfig(config, options) {
59
87
  if (config.isSTI) {
60
88
  dreamImports.push('STI');
61
89
  }
90
+ if (includeSoftDelete) {
91
+ dreamImports.push('SoftDelete');
92
+ }
62
93
  const baseModelName = config.isSTI ? options.fullyQualifiedParentName : config.applicationModelName;
63
94
  const modelImportStatements = [importStatementForModel(baseModelName)];
64
95
  return {
@@ -150,10 +181,11 @@ function buildImportSection(imports) {
150
181
  const modelImports = uniq(imports.modelImportStatements).join('');
151
182
  return `${dreamImportLine}${typeImportLine}${modelImports}`;
152
183
  }
153
- function buildClassDeclaration(config) {
184
+ function buildClassDeclaration(config, { includeSoftDelete }) {
154
185
  const stiDecorator = config.isSTI ? `@STI(${config.parentModelClassName})\n` : '';
186
+ const softDeleteDecorator = includeSoftDelete ? '@SoftDelete()\n' : '';
155
187
  const extendsClause = config.isSTI ? config.parentModelClassName : config.applicationModelName;
156
- return `${stiDecorator}export default class ${config.modelClassName} extends ${extendsClause} {`;
188
+ return `${stiDecorator}${softDeleteDecorator}export default class ${config.modelClassName} extends ${extendsClause} {`;
157
189
  }
158
190
  function buildTableMethod(config) {
159
191
  if (config.isSTI)
@@ -195,14 +227,17 @@ function buildSerializersMethod(config, options) {
195
227
 
196
228
  `;
197
229
  }
198
- function buildFieldsSection(config, attributes) {
230
+ function buildFieldsSection(config, attributes, { includeDeletedAt }) {
199
231
  if (config.isSTI) {
200
232
  return `${attributes.formattedFields}${attributes.formattedDecorators}`;
201
233
  }
202
234
  const idField = ` public id: DreamColumn<${config.modelClassName}, 'id'>`;
235
+ const deletedAtLine = includeDeletedAt
236
+ ? `\n public deletedAt: DreamColumn<${config.modelClassName}, 'deletedAt'>`
237
+ : '';
203
238
  let timestamps = `
204
239
  public createdAt: DreamColumn<${config.modelClassName}, 'createdAt'>
205
- public updatedAt: DreamColumn<${config.modelClassName}, 'updatedAt'>
240
+ public updatedAt: DreamColumn<${config.modelClassName}, 'updatedAt'>${deletedAtLine}
206
241
  `;
207
242
  if (!attributes.formattedDecorators.length) {
208
243
  timestamps = timestamps.replace(/\n$/, '');
@@ -9,7 +9,7 @@ import dreamPath from '../path/dreamPath.js';
9
9
  import snakeify from '../snakeify.js';
10
10
  import generateStiMigrationContent from './generateStiMigrationContent.js';
11
11
  import writeGeneratedFile from './writeGeneratedFile.js';
12
- export default async function generateMigration({ migrationName, columnsWithTypes, connectionName, fullyQualifiedModelName, fullyQualifiedParentName, tableName: explicitTableName, modelClassName, }) {
12
+ export default async function generateMigration({ migrationName, columnsWithTypes, connectionName, fullyQualifiedModelName, fullyQualifiedParentName, tableName: explicitTableName, modelClassName, softDelete = false, }) {
13
13
  const migrationsBasePath = connectionName === 'default'
14
14
  ? path.join(dreamPath('db'), 'migrations')
15
15
  : path.join(dreamPath('db'), 'migrations', connectionName);
@@ -29,6 +29,7 @@ export default async function generateMigration({ migrationName, columnsWithType
29
29
  table: explicitTableName || snakeify(pluralize(pascalizePath(fullyQualifiedModelName))),
30
30
  columnsWithTypes,
31
31
  primaryKeyType: primaryKeyType(connectionName),
32
+ softDelete,
32
33
  });
33
34
  }
34
35
  else {
@@ -9,11 +9,23 @@ import snakeify from '../snakeify.js';
9
9
  import standardizeFullyQualifiedModelName from '../standardizeFullyQualifiedModelName.js';
10
10
  const STI_TYPE_COLUMN_NAME = 'type';
11
11
  const COLUMNS_TO_INDEX = [STI_TYPE_COLUMN_NAME];
12
- export default function generateMigrationContent({ connectionName = 'default', table, columnsWithTypes = [], primaryKeyType = 'bigserial', createOrAlter = 'create', stiChildClassName, } = {}) {
12
+ export default function generateMigrationContent({ connectionName = 'default', table, columnsWithTypes = [], primaryKeyType = 'bigserial', createOrAlter = 'create', stiChildClassName, softDelete = false, } = {}) {
13
13
  const altering = createOrAlter === 'alter';
14
14
  let requireCitextExtension = false;
15
15
  const checkConstraints = [];
16
- const { columnDefs, columnDrops, indexDefs, indexDrops } = columnsWithTypes.reduce((acc, attributeDeclaration) => {
16
+ // When creating a new table, we automatically emit `created_at`,
17
+ // `updated_at`, and (when soft delete is on) `deleted_at` columns. Filter
18
+ // these out of the user-supplied column list so we don't emit duplicate
19
+ // `.addColumn(...)` calls if the user also specified them explicitly.
20
+ const userExplicitlyPassedDeletedAt = !altering && columnsWithTypes.some(col => (col.split(':')[0] ?? '') === 'deleted_at');
21
+ const processedColumnsWithTypes = altering
22
+ ? columnsWithTypes
23
+ : columnsWithTypes.filter(col => {
24
+ const name = col.split(':')[0] ?? '';
25
+ return name !== 'created_at' && name !== 'updated_at' && name !== 'deleted_at';
26
+ });
27
+ const emitDeletedAtColumn = !altering && (softDelete || userExplicitlyPassedDeletedAt);
28
+ const { columnDefs, columnDrops, indexDefs, indexDrops } = processedColumnsWithTypes.reduce((acc, attributeDeclaration) => {
17
29
  const { columnDefs, columnDrops, indexDefs, indexDrops } = acc;
18
30
  const [nonStandardAttributeName, _attributeType, ...descriptors] = attributeDeclaration.split(':');
19
31
  if (!nonStandardAttributeName)
@@ -143,18 +155,20 @@ export async function down(db: Kysely<any>): Promise<void> {
143
155
  const columnDropLines = columnDrops.length
144
156
  ? newlineDoubleIndent + columnDrops.join(newlineDoubleIndent) + newlineDoubleIndent
145
157
  : '';
158
+ const timestampColumnLines = altering
159
+ ? ''
160
+ : newlineDoubleIndent +
161
+ ".addColumn('created_at', 'timestamp', col => col.notNull())" +
162
+ newlineDoubleIndent +
163
+ ".addColumn('updated_at', 'timestamp', col => col.notNull())" +
164
+ (emitDeletedAtColumn ? newlineDoubleIndent + ".addColumn('deleted_at', 'timestamp')" : '');
146
165
  return `\
147
166
  ${dreamDbImports.length ? `import { ${dreamDbImports.join(', ')} } from '@rvoh/dream/db'\n` : ''}import { ${kyselyImports.join(', ')} } from 'kysely'
148
167
 
149
168
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
150
169
  export async function up(db: Kysely<any>): Promise<void> {
151
170
  ${citextExtension}${generateEnumStatements(columnsWithTypes)} await db.schema
152
- .${altering ? 'alterTable' : 'createTable'}('${table}')${altering ? '' : newlineDoubleIndent + generateIdStr({ primaryKeyType })}${columnDefLines}${altering
153
- ? ''
154
- : newlineDoubleIndent +
155
- ".addColumn('created_at', 'timestamp', col => col.notNull())" +
156
- newlineDoubleIndent +
157
- ".addColumn('updated_at', 'timestamp', col => col.notNull())"}
171
+ .${altering ? 'alterTable' : 'createTable'}('${table}')${altering ? '' : newlineDoubleIndent + generateIdStr({ primaryKeyType })}${columnDefLines}${timestampColumnLines}
158
172
  .execute()${indexDefs.length ? `\n${newlineIndent}` : ''}${indexDefs.join(doubleNewlineIndent)}${checkConstraints.join('')}
159
173
  }
160
174
 
@@ -184,7 +198,10 @@ function getAttributeType(attributeType, descriptors) {
184
198
  }
185
199
  function enumAttributeType(descriptors, asArray = false) {
186
200
  const suffix = asArray ? '[]' : '';
187
- return `sql\`${descriptors[0]}_enum${suffix}\``;
201
+ return `sql\`${appendEnumSuffix(descriptors[0])}${suffix}\``;
202
+ }
203
+ function appendEnumSuffix(name) {
204
+ return name?.endsWith('_enum') ? name : `${name}_enum`;
188
205
  }
189
206
  const ENUM_OR_ENUM_ARRAY_REGEX = /:enum:.*:|:enum\[\]:.*:/;
190
207
  function generateEnumStatements(columnsWithTypes) {
@@ -197,7 +214,7 @@ function generateEnumStatements(columnsWithTypes) {
197
214
  return;
198
215
  const columnsWithTypes = columnsWithTypesString.split(/,\s{0,}/);
199
216
  return ` await db.schema
200
- .createType('${enumName}_enum')
217
+ .createType('${appendEnumSuffix(enumName)}')
201
218
  .asEnum([
202
219
  ${columnsWithTypes.map(attr => `'${attr}'`).join(',\n ')}
203
220
  ])
@@ -213,7 +230,7 @@ function generateEnumDropStatements(columnsWithTypes) {
213
230
  const columnsWithTypesString = descriptors[0];
214
231
  if (columnsWithTypesString === undefined)
215
232
  return;
216
- return `await db.schema.dropType('${enumName}_enum').execute()`;
233
+ return `await db.schema.dropType('${appendEnumSuffix(enumName)}').execute()`;
217
234
  }));
218
235
  return finalStatements.length ? '\n\n ' + finalStatements.join('\n ') : '';
219
236
  }
@@ -5,8 +5,53 @@ import isDatabaseArrayColumn from '../helpers/db/types/isDatabaseArrayColumn.js'
5
5
  import CurriedOpsStatement from './curried-ops-statement.js';
6
6
  import OpsStatement from './ops-statement.js';
7
7
  const ops = {
8
+ /**
9
+ * Creates an `OpsStatement` with an arbitrary Kysely comparison operator or trigram operator.
10
+ * Use this as an escape hatch when none of the named helpers cover your use case.
11
+ *
12
+ * @param operator - Any Kysely comparison operator (e.g. `'='`, `'<'`) or trigram operator.
13
+ * @param value - The value to compare against.
14
+ * @returns An `OpsStatement` wrapping the given operator and value.
15
+ *
16
+ * @example
17
+ * User.where({ name: ops.expression('like', '%ello%') })
18
+ */
8
19
  expression: (operator, value) => new OpsStatement(operator, value),
20
+ /**
21
+ * Creates an `OpsStatement` that checks whether a JSONB column contains the given object
22
+ * using the PostgreSQL `@>` (contains) operator.
23
+ *
24
+ * @param searchObj - A plain object whose key/value pairs must be present in the JSONB column.
25
+ * @returns An `OpsStatement` using `@> <searchObj>::jsonb`.
26
+ *
27
+ * @example
28
+ * Post.where({ metadata: ops.jsonb({ published: true }) })
29
+ */
30
+ jsonb: (searchObj) => new OpsStatement('@>', sql `${searchObj}::jsonb`),
31
+ /**
32
+ * Creates an `OpsStatement` for a SQL `IN` clause, matching rows whose column value
33
+ * is one of the provided array elements.
34
+ *
35
+ * @param arr - The array of values to match against.
36
+ * @returns An `OpsStatement` using the `in` operator.
37
+ *
38
+ * @example
39
+ * User.where({ status: ops.in(['active', 'pending']) })
40
+ */
9
41
  in: (arr) => new OpsStatement('in', arr),
42
+ /**
43
+ * Creates a `CurriedOpsStatement` that checks whether a PostgreSQL array column contains
44
+ * the given value using `@> ARRAY[value]::type`. The column type is resolved at query-build
45
+ * time from the Dream model's schema.
46
+ *
47
+ * Throws `AnyRequiresArrayColumn` if the target column is not a database array type.
48
+ *
49
+ * @param value - The scalar value that must be present in the array column.
50
+ * @returns A `CurriedOpsStatement` that resolves to an `OpsStatement` once bound to a model and field.
51
+ *
52
+ * @example
53
+ * Post.where({ tags: ops.any('typescript') })
54
+ */
10
55
  any: (value) => new CurriedOpsStatement(function (dreamClass, fieldName) {
11
56
  const column = fieldName.replace(/^.*\./, '');
12
57
  if (!isDatabaseArrayColumn(dreamClass, column))
@@ -14,27 +59,226 @@ const ops = {
14
59
  const castType = cachedTypeForAttribute(dreamClass, column);
15
60
  return new OpsStatement('@>', sql `ARRAY[${sql.join([value])}]::${sql.raw(castType)}`);
16
61
  }),
62
+ /**
63
+ * Creates an `OpsStatement` using the SQL `LIKE` operator for case-sensitive pattern matching.
64
+ * Use `%` as a wildcard in the pattern string.
65
+ *
66
+ * @param like - The pattern string (e.g. `'%hello%'`).
67
+ * @returns An `OpsStatement` using the `like` operator.
68
+ *
69
+ * @example
70
+ * User.where({ name: ops.like('%alice%') })
71
+ */
17
72
  like: (like) => new OpsStatement('like', like),
73
+ /**
74
+ * Creates an `OpsStatement` using the SQL `ILIKE` operator for case-insensitive pattern matching.
75
+ * Use `%` as a wildcard in the pattern string.
76
+ *
77
+ * @param ilike - The pattern string (e.g. `'%hello%'`).
78
+ * @returns An `OpsStatement` using the `ilike` operator.
79
+ *
80
+ * @example
81
+ * User.where({ name: ops.ilike('%alice%') })
82
+ */
18
83
  ilike: (ilike) => new OpsStatement('ilike', ilike),
84
+ /**
85
+ * Creates an `OpsStatement` that matches a column against a POSIX regular expression.
86
+ * By default the match is case-sensitive (`~`); pass `{ caseInsensitive: true }` to use `~*`.
87
+ *
88
+ * @param match - The regular expression pattern.
89
+ * @param options.caseInsensitive - When `true`, uses the `~*` operator instead of `~`. Defaults to `false`.
90
+ * @returns An `OpsStatement` using `~` or `~*`.
91
+ *
92
+ * @example
93
+ * User.where({ email: ops.match('^admin', { caseInsensitive: true }) })
94
+ */
19
95
  match: (match, { caseInsensitive = false } = {}) => new OpsStatement(caseInsensitive ? '~*' : '~', match),
20
- // current
96
+ /**
97
+ * Creates an `OpsStatement` using the `=` operator for strict equality.
98
+ *
99
+ * @param equal - The value the column must equal.
100
+ * @returns An `OpsStatement` using the `=` operator.
101
+ *
102
+ * @example
103
+ * User.where({ role: ops.equal('admin') })
104
+ */
21
105
  equal: (equal) => new OpsStatement('=', equal),
106
+ /**
107
+ * Creates an `OpsStatement` using the `<` operator.
108
+ *
109
+ * @param lessThan - The value the column must be less than.
110
+ * @returns An `OpsStatement` using the `<` operator.
111
+ *
112
+ * @example
113
+ * Order.where({ total: ops.lessThan(100) })
114
+ */
22
115
  lessThan: (lessThan) => new OpsStatement('<', lessThan),
116
+ /**
117
+ * Creates an `OpsStatement` using the `<=` operator.
118
+ *
119
+ * @param lessThanOrEqualTo - The value the column must be less than or equal to.
120
+ * @returns An `OpsStatement` using the `<=` operator.
121
+ *
122
+ * @example
123
+ * Order.where({ total: ops.lessThanOrEqualTo(100) })
124
+ */
23
125
  lessThanOrEqualTo: (lessThanOrEqualTo) => new OpsStatement('<=', lessThanOrEqualTo),
126
+ /**
127
+ * Creates an `OpsStatement` using the `>` operator.
128
+ *
129
+ * @param greaterThan - The value the column must be greater than.
130
+ * @returns An `OpsStatement` using the `>` operator.
131
+ *
132
+ * @example
133
+ * Order.where({ total: ops.greaterThan(50) })
134
+ */
24
135
  greaterThan: (greaterThan) => new OpsStatement('>', greaterThan),
136
+ /**
137
+ * Creates an `OpsStatement` using the `>=` operator.
138
+ *
139
+ * @param greaterThanOrEqualTo - The value the column must be greater than or equal to.
140
+ * @returns An `OpsStatement` using the `>=` operator.
141
+ *
142
+ * @example
143
+ * Order.where({ total: ops.greaterThanOrEqualTo(50) })
144
+ */
25
145
  greaterThanOrEqualTo: (greaterThanOrEqualTo) => new OpsStatement('>=', greaterThanOrEqualTo),
146
+ /**
147
+ * Creates an `OpsStatement` for PostgreSQL trigram similarity (`%` operator).
148
+ * Rows are included when the similarity score meets or exceeds `score`.
149
+ *
150
+ * Requires the `pg_trgm` extension to be enabled.
151
+ *
152
+ * @param similarity - The string to compare against the column.
153
+ * @param options.score - Minimum similarity threshold (0–1). Defaults to `0.3`.
154
+ * @returns An `OpsStatement` using the `%` trigram operator.
155
+ *
156
+ * @example
157
+ * User.where({ name: ops.similarity('alice', { score: 0.4 }) })
158
+ */
26
159
  similarity: (similarity, { score = 0.3 } = {}) => new OpsStatement('%', similarity, { score }),
160
+ /**
161
+ * Creates an `OpsStatement` for PostgreSQL word similarity (`<%` operator).
162
+ * Rows are included when the word similarity score meets or exceeds `score`.
163
+ *
164
+ * Requires the `pg_trgm` extension to be enabled.
165
+ *
166
+ * @param similarity - The string to compare against the column.
167
+ * @param options.score - Minimum word similarity threshold (0–1). Defaults to `0.5`.
168
+ * @returns An `OpsStatement` using the `<%` trigram operator.
169
+ *
170
+ * @example
171
+ * Article.where({ title: ops.wordSimilarity('postgres', { score: 0.6 }) })
172
+ */
27
173
  wordSimilarity: (similarity, { score = 0.5 } = {}) => new OpsStatement('<%', similarity, { score }),
174
+ /**
175
+ * Creates an `OpsStatement` for PostgreSQL strict word similarity (`<<%` operator).
176
+ * Rows are included when the strict word similarity score meets or exceeds `score`.
177
+ *
178
+ * Requires the `pg_trgm` extension to be enabled.
179
+ *
180
+ * @param similarity - The string to compare against the column.
181
+ * @param options.score - Minimum strict word similarity threshold (0–1). Defaults to `0.6`.
182
+ * @returns An `OpsStatement` using the `<<%` trigram operator.
183
+ *
184
+ * @example
185
+ * Article.where({ title: ops.strictWordSimilarity('postgres', { score: 0.7 }) })
186
+ */
28
187
  strictWordSimilarity: (similarity, { score = 0.6 } = {}) => new OpsStatement('<<%', similarity, { score }),
188
+ /** Negated variants of the standard comparison operators. */
29
189
  not: {
190
+ /**
191
+ * Creates an `OpsStatement` for a SQL `NOT IN` clause, excluding rows whose column value
192
+ * is one of the provided array elements.
193
+ *
194
+ * @param arr - The array of values to exclude.
195
+ * @returns An `OpsStatement` using the `not in` operator.
196
+ *
197
+ * @example
198
+ * User.where({ status: ops.not.in(['banned', 'deleted']) })
199
+ */
30
200
  in: (arr) => new OpsStatement('not in', arr),
201
+ /**
202
+ * Creates an `OpsStatement` using the `NOT LIKE` operator for case-sensitive pattern exclusion.
203
+ *
204
+ * @param like - The pattern string (e.g. `'%spam%'`).
205
+ * @returns An `OpsStatement` using the `not like` operator.
206
+ *
207
+ * @example
208
+ * User.where({ email: ops.not.like('%@example.com') })
209
+ */
31
210
  like: (like) => new OpsStatement('not like', like),
211
+ /**
212
+ * Creates an `OpsStatement` using the `NOT ILIKE` operator for case-insensitive pattern exclusion.
213
+ *
214
+ * @param ilike - The pattern string (e.g. `'%spam%'`).
215
+ * @returns An `OpsStatement` using the `not ilike` operator.
216
+ *
217
+ * @example
218
+ * User.where({ email: ops.not.ilike('%@example.com') })
219
+ */
32
220
  ilike: (ilike) => new OpsStatement('not ilike', ilike),
221
+ /**
222
+ * Creates an `OpsStatement` that excludes rows matching a POSIX regular expression.
223
+ * By default the match is case-sensitive (`!~`); pass `{ caseInsensitive: true }` to use `!~*`.
224
+ *
225
+ * @param match - The regular expression pattern.
226
+ * @param options.caseInsensitive - When `true`, uses `!~*` instead of `!~`. Defaults to `false`.
227
+ * @returns An `OpsStatement` using `!~` or `!~*`.
228
+ *
229
+ * @example
230
+ * User.where({ email: ops.not.match('^admin', { caseInsensitive: true }) })
231
+ */
33
232
  match: (match, { caseInsensitive = false } = {}) => new OpsStatement(caseInsensitive ? '!~*' : '!~', match),
233
+ /**
234
+ * Creates an `OpsStatement` using the `!=` operator for inequality.
235
+ *
236
+ * @param equal - The value the column must not equal.
237
+ * @returns An `OpsStatement` using the `!=` operator.
238
+ *
239
+ * @example
240
+ * User.where({ role: ops.not.equal('guest') })
241
+ */
34
242
  equal: (equal) => new OpsStatement('!=', equal),
243
+ /**
244
+ * Creates an `OpsStatement` that negates `<`, equivalent to `>=`.
245
+ *
246
+ * @param lessThan - The value used as the lower bound (inclusive).
247
+ * @returns An `OpsStatement` using `>=`.
248
+ *
249
+ * @example
250
+ * Order.where({ total: ops.not.lessThan(100) }) // total >= 100
251
+ */
35
252
  lessThan: (lessThan) => new OpsStatement('>=', lessThan),
253
+ /**
254
+ * Creates an `OpsStatement` that negates `<=`, equivalent to `>`.
255
+ *
256
+ * @param lessThanOrEqualTo - The value used as the lower bound (exclusive).
257
+ * @returns An `OpsStatement` using `>`.
258
+ *
259
+ * @example
260
+ * Order.where({ total: ops.not.lessThanOrEqualTo(100) }) // total > 100
261
+ */
36
262
  lessThanOrEqualTo: (lessThanOrEqualTo) => new OpsStatement('>', lessThanOrEqualTo),
263
+ /**
264
+ * Creates an `OpsStatement` that negates `>`, equivalent to `<=`.
265
+ *
266
+ * @param greaterThan - The value used as the upper bound (inclusive).
267
+ * @returns An `OpsStatement` using `<=`.
268
+ *
269
+ * @example
270
+ * Order.where({ total: ops.not.greaterThan(50) }) // total <= 50
271
+ */
37
272
  greaterThan: (greaterThan) => new OpsStatement('<=', greaterThan),
273
+ /**
274
+ * Creates an `OpsStatement` that negates `>=`, equivalent to `<`.
275
+ *
276
+ * @param greaterThanOrEqualTo - The value used as the upper bound (exclusive).
277
+ * @returns An `OpsStatement` using `<`.
278
+ *
279
+ * @example
280
+ * Order.where({ total: ops.not.greaterThanOrEqualTo(50) }) // total < 50
281
+ */
38
282
  greaterThanOrEqualTo: (greaterThanOrEqualTo) => new OpsStatement('<', greaterThanOrEqualTo),
39
283
  },
40
284
  };
@@ -62,6 +62,8 @@ export default class DreamBin {
62
62
  includeInternalSerializers: options.internalSerializers ?? false,
63
63
  ...options,
64
64
  stiBaseSerializer: false,
65
+ // `@SoftDelete()` is incompatible with STI children — never auto-apply.
66
+ softDelete: false,
65
67
  },
66
68
  fullyQualifiedParentName,
67
69
  });
@@ -190,6 +190,7 @@ ${INDENT} pnpm psy g:model --model-name=GroupDanceLesson Lesson/Dance/Group
190
190
  ${INDENT} # model is named GroupDanceLesson instead of LessonDanceGroup`)
191
191
  .option('--admin-serializers', 'also generate AdminSerializer and AdminSummarySerializer variants for admin-facing API endpoints that may expose additional fields', false)
192
192
  .option('--internal-serializers', 'also generate InternalSerializer and InternalSummarySerializer variants for internal API endpoints that may expose additional fields', false)
193
+ .option('--no-soft-delete', `skip generating the @SoftDelete() decorator and the corresponding nullable \`deleted_at\` column. By default, generated models use soft-delete semantics (rows are marked deleted via \`deleted_at\` instead of being removed from the database). Pass this flag when you want records to be hard-deleted.`)
193
194
  .argument('<modelName>', `The fully qualified model name, using / for namespacing. This determines the model class name (may be overridden with \`--model-name\`), table name, and file path under src/app/models/.
194
195
  ${INDENT}
195
196
  ${INDENT}Examples:
@@ -9,6 +9,11 @@ import writeGeneratedFile from './writeGeneratedFile.js';
9
9
  export default async function generateDream({ fullyQualifiedModelName, columnsWithTypes, options, fullyQualifiedParentName, }) {
10
10
  fullyQualifiedModelName = standardizeFullyQualifiedModelName(fullyQualifiedModelName);
11
11
  const modelClassName = modelClassNameFrom(fullyQualifiedModelName, options.modelName);
12
+ const isSTI = !!fullyQualifiedParentName;
13
+ // `@SoftDelete()` is incompatible with STI children, so we force-disable
14
+ // the soft-delete scaffold when generating an STI child regardless of the
15
+ // caller-provided value.
16
+ const softDelete = !isSTI && !!options.softDelete;
12
17
  await writeGeneratedFile({
13
18
  dreamPathKey: 'models',
14
19
  fileName: `${fullyQualifiedModelName}.ts`,
@@ -22,6 +27,7 @@ export default async function generateDream({ fullyQualifiedModelName, columnsWi
22
27
  connectionName: options.connectionName,
23
28
  tableName: options.tableName,
24
29
  modelClassName,
30
+ softDelete,
25
31
  }),
26
32
  logLabel: 'dream',
27
33
  });
@@ -37,7 +43,6 @@ export default async function generateDream({ fullyQualifiedModelName, columnsWi
37
43
  includeInternalSerializers: options.includeInternalSerializers ?? false,
38
44
  modelClassName,
39
45
  });
40
- const isSTI = !!fullyQualifiedParentName;
41
46
  if (columnsWithTypes.length || !isSTI) {
42
47
  await generateMigration({
43
48
  connectionName: options.connectionName,
@@ -47,6 +52,7 @@ export default async function generateDream({ fullyQualifiedModelName, columnsWi
47
52
  fullyQualifiedParentName,
48
53
  tableName: options.tableName,
49
54
  modelClassName,
55
+ softDelete,
50
56
  });
51
57
  }
52
58
  }