@rvoh/dream 2.6.1-alpha.1 → 2.7.1

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 (204) 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/helpers/cli/generateSerializerContent.js +53 -50
  8. package/dist/cjs/src/ops/index.js +245 -1
  9. package/dist/esm/src/bin/index.js +2 -0
  10. package/dist/esm/src/cli/index.js +1 -0
  11. package/dist/esm/src/helpers/cli/generateDream.js +7 -1
  12. package/dist/esm/src/helpers/cli/generateDreamContent.js +44 -9
  13. package/dist/esm/src/helpers/cli/generateMigration.js +2 -1
  14. package/dist/esm/src/helpers/cli/generateMigrationContent.js +28 -11
  15. package/dist/esm/src/helpers/cli/generateSerializerContent.js +53 -50
  16. package/dist/esm/src/ops/index.js +245 -1
  17. package/dist/types/src/bin/index.d.ts +1 -0
  18. package/dist/types/src/cli/index.d.ts +7 -0
  19. package/dist/types/src/helpers/cli/generateDream.d.ts +8 -0
  20. package/dist/types/src/helpers/cli/generateDreamContent.d.ts +11 -1
  21. package/dist/types/src/helpers/cli/generateMigration.d.ts +7 -1
  22. package/dist/types/src/helpers/cli/generateMigrationContent.d.ts +7 -1
  23. package/dist/types/src/ops/index.d.ts +245 -0
  24. package/dist/types/src/types/associations/shared.d.ts +2 -2
  25. package/dist/types/src/types/associations/shared.ts +17 -11
  26. package/docs/assets/search.js +1 -1
  27. package/docs/classes/db.DreamMigrationHelpers.html +9 -9
  28. package/docs/classes/db.KyselyQueryDriver.html +32 -32
  29. package/docs/classes/db.PostgresQueryDriver.html +33 -33
  30. package/docs/classes/db.QueryDriverBase.html +31 -31
  31. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  32. package/docs/classes/errors.ColumnOverflow.html +3 -3
  33. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  34. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  35. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  36. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  37. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  38. package/docs/classes/errors.InvalidClockTime.html +2 -2
  39. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  40. package/docs/classes/errors.InvalidDateTime.html +2 -2
  41. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  42. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  43. package/docs/classes/errors.NotNullViolation.html +3 -3
  44. package/docs/classes/errors.RecordNotFound.html +3 -3
  45. package/docs/classes/errors.ValidationError.html +3 -3
  46. package/docs/classes/index.CalendarDate.html +33 -33
  47. package/docs/classes/index.ClockTime.html +32 -32
  48. package/docs/classes/index.ClockTimeTz.html +35 -35
  49. package/docs/classes/index.DateTime.html +86 -86
  50. package/docs/classes/index.Decorators.html +19 -19
  51. package/docs/classes/index.Dream.html +119 -119
  52. package/docs/classes/index.DreamApp.html +5 -5
  53. package/docs/classes/index.DreamTransaction.html +2 -2
  54. package/docs/classes/index.Env.html +2 -2
  55. package/docs/classes/index.Query.html +56 -56
  56. package/docs/classes/system.CliFileWriter.html +4 -4
  57. package/docs/classes/system.DreamBin.html +2 -2
  58. package/docs/classes/system.DreamCLI.html +6 -6
  59. package/docs/classes/system.DreamImporter.html +2 -2
  60. package/docs/classes/system.DreamLogos.html +2 -2
  61. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  62. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  63. package/docs/classes/system.PathHelpers.html +3 -3
  64. package/docs/classes/utils.Encrypt.html +2 -2
  65. package/docs/classes/utils.Range.html +2 -2
  66. package/docs/functions/db.closeAllDbConnections.html +1 -1
  67. package/docs/functions/db.dreamDbConnections.html +1 -1
  68. package/docs/functions/db.untypedDb.html +1 -1
  69. package/docs/functions/db.validateColumn.html +1 -1
  70. package/docs/functions/db.validateTable.html +1 -1
  71. package/docs/functions/errors.pgErrorType.html +1 -1
  72. package/docs/functions/index.DreamSerializer.html +1 -1
  73. package/docs/functions/index.ObjectSerializer.html +1 -1
  74. package/docs/functions/index.ReplicaSafe.html +1 -1
  75. package/docs/functions/index.STI.html +1 -1
  76. package/docs/functions/index.SoftDelete.html +1 -1
  77. package/docs/functions/utils.camelize.html +1 -1
  78. package/docs/functions/utils.capitalize.html +1 -1
  79. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  80. package/docs/functions/utils.compact.html +1 -1
  81. package/docs/functions/utils.groupBy.html +1 -1
  82. package/docs/functions/utils.hyphenize.html +1 -1
  83. package/docs/functions/utils.intersection.html +1 -1
  84. package/docs/functions/utils.isEmpty.html +1 -1
  85. package/docs/functions/utils.normalizeUnicode.html +1 -1
  86. package/docs/functions/utils.pascalize.html +1 -1
  87. package/docs/functions/utils.percent.html +1 -1
  88. package/docs/functions/utils.range.html +1 -1
  89. package/docs/functions/utils.round.html +1 -1
  90. package/docs/functions/utils.sanitizeString.html +1 -1
  91. package/docs/functions/utils.snakeify.html +1 -1
  92. package/docs/functions/utils.sort.html +1 -1
  93. package/docs/functions/utils.sortBy.html +1 -1
  94. package/docs/functions/utils.sortObjectByKey.html +1 -1
  95. package/docs/functions/utils.sortObjectByValue.html +1 -1
  96. package/docs/functions/utils.uncapitalize.html +1 -1
  97. package/docs/functions/utils.uniq.html +1 -1
  98. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  99. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  100. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  101. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  102. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  103. package/docs/interfaces/types.DecoratorContext.html +2 -2
  104. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  105. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  106. package/docs/interfaces/types.DurationObject.html +2 -2
  107. package/docs/interfaces/types.EncryptOptions.html +2 -2
  108. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  109. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  110. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  111. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  112. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  113. package/docs/types/openapi.OpenapiFormats.html +1 -1
  114. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  115. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  116. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  117. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  118. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  119. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  120. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  121. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  122. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  123. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  124. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  125. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  126. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  127. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  128. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  129. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  130. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  131. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  132. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  133. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  134. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  135. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  136. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  137. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  138. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  139. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  140. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  141. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  142. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  143. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  144. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  145. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  146. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  147. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  148. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  149. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  150. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  151. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  152. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  153. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  154. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  155. package/docs/types/types.CalendarDateObject.html +1 -1
  156. package/docs/types/types.Camelized.html +1 -1
  157. package/docs/types/types.ClockTimeObject.html +1 -1
  158. package/docs/types/types.DbConnectionType.html +1 -1
  159. package/docs/types/types.DbTypes.html +1 -1
  160. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  161. package/docs/types/types.DreamAttributes.html +1 -1
  162. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  163. package/docs/types/types.DreamClassColumn.html +1 -1
  164. package/docs/types/types.DreamColumn.html +1 -1
  165. package/docs/types/types.DreamColumnNames.html +1 -1
  166. package/docs/types/types.DreamLogLevel.html +1 -1
  167. package/docs/types/types.DreamLogger.html +2 -2
  168. package/docs/types/types.DreamModelSerializerType.html +1 -1
  169. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  170. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  171. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  172. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  173. package/docs/types/types.DreamSerializable.html +1 -1
  174. package/docs/types/types.DreamSerializableArray.html +1 -1
  175. package/docs/types/types.DreamSerializerKey.html +1 -1
  176. package/docs/types/types.DreamSerializers.html +1 -1
  177. package/docs/types/types.DreamVirtualColumns.html +1 -1
  178. package/docs/types/types.DurationUnit.html +1 -1
  179. package/docs/types/types.EncryptAlgorithm.html +1 -1
  180. package/docs/types/types.HasManyStatement.html +1 -1
  181. package/docs/types/types.HasOneStatement.html +1 -1
  182. package/docs/types/types.Hyphenized.html +1 -1
  183. package/docs/types/types.Pascalized.html +1 -1
  184. package/docs/types/types.PrimaryKeyType.html +1 -1
  185. package/docs/types/types.RoundingPrecision.html +1 -1
  186. package/docs/types/types.SerializerCasing.html +1 -1
  187. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  188. package/docs/types/types.Snakeified.html +1 -1
  189. package/docs/types/types.StrictInterface.html +1 -1
  190. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  191. package/docs/types/types.UpdateableProperties.html +1 -1
  192. package/docs/types/types.ValidationType.html +1 -1
  193. package/docs/types/types.ViewModel.html +2 -2
  194. package/docs/types/types.ViewModelClass.html +1 -1
  195. package/docs/types/types.WeekdayName.html +1 -1
  196. package/docs/types/types.WhereStatementForDream.html +1 -1
  197. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  198. package/docs/variables/index.DreamConst.html +1 -1
  199. package/docs/variables/index.ops.html +115 -1
  200. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  201. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  202. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  203. package/docs/variables/system.primaryKeyTypes.html +1 -1
  204. 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
  }
@@ -1,4 +1,3 @@
1
- import serializerNameFromFullyQualifiedModelName from '../../serializer/helpers/serializerNameFromFullyQualifiedModelName.js';
2
1
  import camelize from '../camelize.js';
3
2
  import globalClassNameFromFullyQualifiedModelName from '../globalClassNameFromFullyQualifiedModelName.js';
4
3
  import absoluteDreamPath from '../path/absoluteDreamPath.js';
@@ -9,9 +8,18 @@ export default function generateSerializerContent({ fullyQualifiedModelName, col
9
8
  const additionalImports = [];
10
9
  const dreamImports = [];
11
10
  const isSTI = !!fullyQualifiedParentName;
11
+ // Variants that should be emitted in this file. Each variant generates a
12
+ // matching summary + default serializer pair; STI children import the
13
+ // corresponding variant from the parent module so the inheritance chain
14
+ // stays within the same variant (admin → admin, internal → internal).
15
+ const variantSuffixes = [''];
16
+ if (includeAdminSerializers)
17
+ variantSuffixes.push('Admin');
18
+ if (includeInternalSerializers)
19
+ variantSuffixes.push('Internal');
12
20
  if (isSTI) {
13
21
  fullyQualifiedParentName = standardizeFullyQualifiedModelName(fullyQualifiedParentName);
14
- additionalImports.push(importStatementForSerializer(fullyQualifiedModelName, fullyQualifiedParentName));
22
+ additionalImports.push(importStatementForSerializer(fullyQualifiedModelName, fullyQualifiedParentName, variantSuffixes));
15
23
  }
16
24
  else {
17
25
  dreamImports.push('DreamSerializer');
@@ -23,58 +31,50 @@ export default function generateSerializerContent({ fullyQualifiedModelName, col
23
31
  : `(${modelInstanceName}: ${modelClassName})`;
24
32
  const modelSerializerArgs = `${modelInstanceName}`;
25
33
  const dreamSerializerArgs = `${stiBaseSerializer ? `StiChildClass ?? ${modelClassName}` : modelClassName}, ${modelInstanceName}`;
26
- const serializerClassName = serializerNameFromFullyQualifiedModelName(fullyQualifiedModelNameToSerializerBaseName(fullyQualifiedModelName));
27
- const summarySerializerClassName = serializerNameFromFullyQualifiedModelName(fullyQualifiedModelNameToSerializerBaseName(fullyQualifiedModelName), 'summary');
28
- const defaultSerializerExtends = isSTI
29
- ? `${serializerNameFromFullyQualifiedModelName(fullyQualifiedModelNameToSerializerBaseName(fullyQualifiedParentName))}(${modelClassName}, ${modelSerializerArgs})`
30
- : `${summarySerializerClassName}(${stiBaseSerializer ? 'StiChildClass, ' : ''}${modelSerializerArgs})`;
31
- const summarySerializerExtends = isSTI
32
- ? `${serializerNameFromFullyQualifiedModelName(fullyQualifiedModelNameToSerializerBaseName(fullyQualifiedParentName), 'summary')}(${modelClassName}, ${modelSerializerArgs})`
33
- : `DreamSerializer(${dreamSerializerArgs})`;
34
- // Admin variants
35
- const adminSerializerClassName = serializerClassName.replace(/Serializer$/, 'AdminSerializer');
36
- const adminSummarySerializerClassName = summarySerializerClassName.replace(/SummarySerializer$/, 'AdminSummarySerializer');
37
- // end:Admin variants
38
- // Internal variants
39
- const internalSerializerClassName = serializerClassName.replace(/Serializer$/, 'InternalSerializer');
40
- const internalSummarySerializerClassName = summarySerializerClassName.replace(/SummarySerializer$/, 'InternalSummarySerializer');
41
- // end:Internal variants
34
+ const localSerializerBase = fullyQualifiedModelNameToSerializerBaseName(fullyQualifiedModelName);
35
+ const parentSerializerBase = isSTI
36
+ ? fullyQualifiedModelNameToSerializerBaseName(fullyQualifiedParentName)
37
+ : '';
42
38
  const additionalModelImports = [];
43
39
  const dreamImport = dreamImports.length
44
40
  ? `import { ${uniq(dreamImports).join(', ')} } from '@rvoh/dream'\n`
45
41
  : '';
46
42
  const additionalImportsStr = uniq(additionalImports).join('');
47
- const summarySerializer = `export const ${summarySerializerClassName} = ${modelSerializerSignature} =>
48
- ${summarySerializerExtends}${isSTI ? '' : `\n .attribute('id')`}`;
49
- const defaultSerializer = `export const ${serializerClassName} = ${modelSerializerSignature} =>
50
- ${defaultSerializerExtends}${columnsWithTypes
51
- .map(attr => {
52
- const [name, type] = attr.split(':');
53
- if (name === undefined)
54
- return '';
55
- if (['belongsto', 'hasone', 'hasmany'].includes(camelize(type)?.toLowerCase()))
56
- return '';
57
- return `\n ${attribute(modelClassName, name, type, attr, stiBaseSerializer)}`;
58
- })
59
- .join('')}`;
43
+ const buildSerializerPair = (variantSuffix) => {
44
+ const summaryName = variantSerializerClassName(localSerializerBase, variantSuffix, 'summary');
45
+ const defaultName = variantSerializerClassName(localSerializerBase, variantSuffix, 'default');
46
+ const summaryExtends = isSTI
47
+ ? `${variantSerializerClassName(parentSerializerBase, variantSuffix, 'summary')}(${modelClassName}, ${modelSerializerArgs})`
48
+ : `DreamSerializer(${dreamSerializerArgs})`;
49
+ const defaultExtends = isSTI
50
+ ? `${variantSerializerClassName(parentSerializerBase, variantSuffix, 'default')}(${modelClassName}, ${modelSerializerArgs})`
51
+ : `${summaryName}(${stiBaseSerializer ? 'StiChildClass, ' : ''}${modelSerializerArgs})`;
52
+ const summary = `export const ${summaryName} = ${modelSerializerSignature} =>
53
+ ${summaryExtends}${isSTI ? '' : `\n .attribute('id')`}`;
54
+ const defaultBody = columnsWithTypes
55
+ .map(attr => {
56
+ const [name, type] = attr.split(':');
57
+ if (name === undefined)
58
+ return '';
59
+ if (['belongsto', 'hasone', 'hasmany'].includes(camelize(type)?.toLowerCase()))
60
+ return '';
61
+ return `\n ${attribute(modelClassName, name, type, attr, stiBaseSerializer)}`;
62
+ })
63
+ .join('');
64
+ const def = `export const ${defaultName} = ${modelSerializerSignature} =>
65
+ ${defaultExtends}${defaultBody}`;
66
+ return `${summary}\n\n${def}`;
67
+ };
68
+ const serializerBlocks = variantSuffixes.map(buildSerializerPair).join('\n\n');
60
69
  return `${dreamImport}${additionalImportsStr}${relatedModelImport}${additionalModelImports.join('')}
61
- ${summarySerializer}
62
-
63
- ${defaultSerializer}${!includeAdminSerializers
64
- ? ''
65
- : `
66
-
67
- ${summarySerializer.replace(summarySerializerClassName, adminSummarySerializerClassName)}
68
-
69
- ${defaultSerializer.replace(serializerClassName, adminSerializerClassName).replace(summarySerializerClassName, adminSummarySerializerClassName)}`}${!includeInternalSerializers
70
- ? ''
71
- : `
72
-
73
- ${summarySerializer.replace(summarySerializerClassName, internalSummarySerializerClassName)}
74
-
75
- ${defaultSerializer.replace(serializerClassName, internalSerializerClassName).replace(summarySerializerClassName, internalSummarySerializerClassName)}`}
70
+ ${serializerBlocks}
76
71
  `;
77
72
  }
73
+ function variantSerializerClassName(serializerBase, variantSuffix, kind) {
74
+ return kind === 'summary'
75
+ ? `${serializerBase}${variantSuffix}SummarySerializer`
76
+ : `${serializerBase}${variantSuffix}Serializer`;
77
+ }
78
78
  function attribute(modelClassName, name, type, attr, stiBaseSerializer) {
79
79
  if (name === 'type' && stiBaseSerializer) {
80
80
  return `.attribute('type', { openapi: { type: 'string', enum: [(StiChildClass ?? ${modelClassName}).sanitizedName] } })`;
@@ -97,11 +97,14 @@ function attributeOptionsSpecifier(type, attr) {
97
97
  return '';
98
98
  }
99
99
  }
100
- function importStatementForSerializer(originModelName, destinationModelName) {
101
- const defaultSerializer = globalClassNameFromFullyQualifiedModelName(serializerNameFromFullyQualifiedModelName(fullyQualifiedModelNameToSerializerBaseName(destinationModelName)));
102
- const summarySerializer = globalClassNameFromFullyQualifiedModelName(serializerNameFromFullyQualifiedModelName(fullyQualifiedModelNameToSerializerBaseName(destinationModelName), 'summary'));
100
+ function importStatementForSerializer(originModelName, destinationModelName, variantSuffixes) {
101
+ const base = fullyQualifiedModelNameToSerializerBaseName(destinationModelName);
102
+ const names = variantSuffixes.flatMap(suffix => [
103
+ globalClassNameFromFullyQualifiedModelName(variantSerializerClassName(base, suffix, 'default')),
104
+ globalClassNameFromFullyQualifiedModelName(variantSerializerClassName(base, suffix, 'summary')),
105
+ ]);
103
106
  const importFrom = absoluteDreamPath('serializers', destinationModelName);
104
- return `import { ${defaultSerializer}, ${summarySerializer} } from '${importFrom}'\n`;
107
+ return `import { ${names.join(', ')} } from '${importFrom}'\n`;
105
108
  }
106
109
  function importStatementForModel(modelClassName, fullyQualifiedModelName) {
107
110
  const importFrom = absoluteDreamPath('models', fullyQualifiedModelName);