@rvoh/dream 2.8.1 → 2.9.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 (221) hide show
  1. package/dist/cjs/src/bin/index.js +17 -2
  2. package/dist/cjs/src/cli/index.js +88 -12
  3. package/dist/cjs/src/db/helpers/syncDbTypesFiles.js +17 -12
  4. package/dist/cjs/src/dream/QueryDriver/Kysely.js +21 -10
  5. package/dist/cjs/src/encrypt/algorithms/aes-gcm/decryptAESGCM.js +19 -6
  6. package/dist/cjs/src/encrypt/index.js +80 -11
  7. package/dist/cjs/src/errors/SspawnRequiresDevelopmentOrTest.js +26 -0
  8. package/dist/cjs/src/errors/encrypt/DecryptionError.js +5 -0
  9. package/dist/cjs/src/errors/encrypt/DecryptionParseError.js +5 -0
  10. package/dist/cjs/src/errors/encrypt/DecryptionWithRotationError.js +12 -0
  11. package/dist/cjs/src/helpers/cli/generateMigrationContent.js +2 -1
  12. package/dist/cjs/src/ops/index.js +28 -1
  13. package/dist/cjs/src/serializer/builders/DreamSerializerBuilder.js +29 -10
  14. package/dist/cjs/src/serializer/builders/ObjectSerializerBuilder.js +20 -4
  15. package/dist/esm/src/bin/index.js +17 -2
  16. package/dist/esm/src/cli/index.js +88 -12
  17. package/dist/esm/src/db/helpers/syncDbTypesFiles.js +17 -12
  18. package/dist/esm/src/dream/QueryDriver/Kysely.js +21 -10
  19. package/dist/esm/src/encrypt/algorithms/aes-gcm/decryptAESGCM.js +19 -6
  20. package/dist/esm/src/encrypt/index.js +80 -11
  21. package/dist/esm/src/errors/SspawnRequiresDevelopmentOrTest.js +26 -0
  22. package/dist/esm/src/errors/encrypt/DecryptionError.js +5 -0
  23. package/dist/esm/src/errors/encrypt/DecryptionParseError.js +5 -0
  24. package/dist/esm/src/errors/encrypt/DecryptionWithRotationError.js +12 -0
  25. package/dist/esm/src/helpers/cli/generateMigrationContent.js +2 -1
  26. package/dist/esm/src/ops/index.js +28 -1
  27. package/dist/esm/src/serializer/builders/DreamSerializerBuilder.js +29 -10
  28. package/dist/esm/src/serializer/builders/ObjectSerializerBuilder.js +20 -4
  29. package/dist/types/src/cli/index.d.ts +57 -4
  30. package/dist/types/src/dream/QueryDriver/Kysely.d.ts +17 -0
  31. package/dist/types/src/dream-app/index.d.ts +25 -1
  32. package/dist/types/src/encrypt/algorithms/aes-gcm/decryptAESGCM.d.ts +1 -1
  33. package/dist/types/src/encrypt/index.d.ts +59 -0
  34. package/dist/types/src/errors/SspawnRequiresDevelopmentOrTest.d.ts +5 -0
  35. package/dist/types/src/errors/encrypt/DecryptionError.d.ts +3 -0
  36. package/dist/types/src/errors/encrypt/DecryptionParseError.d.ts +3 -0
  37. package/dist/types/src/errors/encrypt/DecryptionWithRotationError.d.ts +7 -0
  38. package/dist/types/src/ops/index.d.ts +28 -1
  39. package/dist/types/src/serializer/builders/DreamSerializerBuilder.d.ts +40 -12
  40. package/dist/types/src/serializer/builders/ObjectSerializerBuilder.d.ts +23 -5
  41. package/docs/classes/db.DreamMigrationHelpers.html +9 -9
  42. package/docs/classes/db.KyselyQueryDriver.html +32 -32
  43. package/docs/classes/db.PostgresQueryDriver.html +33 -33
  44. package/docs/classes/db.QueryDriverBase.html +31 -31
  45. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  46. package/docs/classes/errors.ColumnOverflow.html +3 -3
  47. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  48. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  49. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  50. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  51. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  52. package/docs/classes/errors.InvalidClockTime.html +2 -2
  53. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  54. package/docs/classes/errors.InvalidDateTime.html +2 -2
  55. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  56. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  57. package/docs/classes/errors.NotNullViolation.html +3 -3
  58. package/docs/classes/errors.RecordNotFound.html +3 -3
  59. package/docs/classes/errors.ValidationError.html +3 -3
  60. package/docs/classes/index.CalendarDate.html +33 -33
  61. package/docs/classes/index.ClockTime.html +32 -32
  62. package/docs/classes/index.ClockTimeTz.html +35 -35
  63. package/docs/classes/index.DateTime.html +86 -86
  64. package/docs/classes/index.Decorators.html +19 -19
  65. package/docs/classes/index.Dream.html +118 -118
  66. package/docs/classes/index.DreamApp.html +5 -5
  67. package/docs/classes/index.DreamTransaction.html +2 -2
  68. package/docs/classes/index.Env.html +2 -2
  69. package/docs/classes/index.Query.html +56 -56
  70. package/docs/classes/system.CliFileWriter.html +4 -4
  71. package/docs/classes/system.DreamBin.html +2 -2
  72. package/docs/classes/system.DreamCLI.html +41 -6
  73. package/docs/classes/system.DreamImporter.html +2 -2
  74. package/docs/classes/system.DreamLogos.html +2 -2
  75. package/docs/classes/system.DreamSerializerBuilder.html +45 -23
  76. package/docs/classes/system.ObjectSerializerBuilder.html +32 -13
  77. package/docs/classes/system.PathHelpers.html +3 -3
  78. package/docs/classes/utils.Encrypt.html +51 -2
  79. package/docs/classes/utils.Range.html +2 -2
  80. package/docs/functions/db.closeAllDbConnections.html +1 -1
  81. package/docs/functions/db.dreamDbConnections.html +1 -1
  82. package/docs/functions/db.untypedDb.html +1 -1
  83. package/docs/functions/db.validateColumn.html +1 -1
  84. package/docs/functions/db.validateTable.html +1 -1
  85. package/docs/functions/errors.pgErrorType.html +1 -1
  86. package/docs/functions/index.DreamSerializer.html +1 -1
  87. package/docs/functions/index.ObjectSerializer.html +1 -1
  88. package/docs/functions/index.ReplicaSafe.html +1 -1
  89. package/docs/functions/index.STI.html +1 -1
  90. package/docs/functions/index.SoftDelete.html +1 -1
  91. package/docs/functions/utils.camelize.html +1 -1
  92. package/docs/functions/utils.capitalize.html +1 -1
  93. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  94. package/docs/functions/utils.compact.html +1 -1
  95. package/docs/functions/utils.groupBy.html +1 -1
  96. package/docs/functions/utils.hyphenize.html +1 -1
  97. package/docs/functions/utils.intersection.html +1 -1
  98. package/docs/functions/utils.isEmpty.html +1 -1
  99. package/docs/functions/utils.normalizeUnicode.html +1 -1
  100. package/docs/functions/utils.pascalize.html +1 -1
  101. package/docs/functions/utils.percent.html +1 -1
  102. package/docs/functions/utils.range.html +1 -1
  103. package/docs/functions/utils.round.html +1 -1
  104. package/docs/functions/utils.sanitizeString.html +1 -1
  105. package/docs/functions/utils.snakeify.html +1 -1
  106. package/docs/functions/utils.sort.html +1 -1
  107. package/docs/functions/utils.sortBy.html +1 -1
  108. package/docs/functions/utils.sortObjectByKey.html +1 -1
  109. package/docs/functions/utils.sortObjectByValue.html +1 -1
  110. package/docs/functions/utils.uncapitalize.html +1 -1
  111. package/docs/functions/utils.uniq.html +1 -1
  112. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  113. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  114. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  115. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  116. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  117. package/docs/interfaces/types.DecoratorContext.html +2 -2
  118. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  119. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  120. package/docs/interfaces/types.DurationObject.html +2 -2
  121. package/docs/interfaces/types.EncryptOptions.html +2 -2
  122. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  123. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  124. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  125. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  126. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  127. package/docs/types/openapi.OpenapiFormats.html +1 -1
  128. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  129. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  130. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  131. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  132. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  133. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  134. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  135. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  136. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  137. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  138. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  139. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  140. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  141. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  142. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  143. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  144. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  145. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  146. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  147. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  148. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  149. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  150. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  151. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  152. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  153. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  154. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  155. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  156. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  157. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  158. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  159. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  160. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  161. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  162. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  163. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  164. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  165. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  166. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  167. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  168. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  169. package/docs/types/types.CalendarDateObject.html +1 -1
  170. package/docs/types/types.Camelized.html +1 -1
  171. package/docs/types/types.ClockTimeObject.html +1 -1
  172. package/docs/types/types.DbConnectionType.html +1 -1
  173. package/docs/types/types.DbTypes.html +1 -1
  174. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  175. package/docs/types/types.DreamAttributes.html +1 -1
  176. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  177. package/docs/types/types.DreamClassColumn.html +1 -1
  178. package/docs/types/types.DreamColumn.html +1 -1
  179. package/docs/types/types.DreamColumnNames.html +1 -1
  180. package/docs/types/types.DreamLogLevel.html +1 -1
  181. package/docs/types/types.DreamLogger.html +2 -2
  182. package/docs/types/types.DreamModelSerializerType.html +1 -1
  183. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  184. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  185. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  186. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  187. package/docs/types/types.DreamSerializable.html +1 -1
  188. package/docs/types/types.DreamSerializableArray.html +1 -1
  189. package/docs/types/types.DreamSerializerKey.html +1 -1
  190. package/docs/types/types.DreamSerializers.html +1 -1
  191. package/docs/types/types.DreamVirtualColumns.html +1 -1
  192. package/docs/types/types.DurationUnit.html +1 -1
  193. package/docs/types/types.EncryptAlgorithm.html +1 -1
  194. package/docs/types/types.HasManyStatement.html +1 -1
  195. package/docs/types/types.HasOneStatement.html +1 -1
  196. package/docs/types/types.Hyphenized.html +1 -1
  197. package/docs/types/types.Pascalized.html +1 -1
  198. package/docs/types/types.PrimaryKeyType.html +1 -1
  199. package/docs/types/types.RoundingPrecision.html +1 -1
  200. package/docs/types/types.SerializerCasing.html +1 -1
  201. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  202. package/docs/types/types.Snakeified.html +1 -1
  203. package/docs/types/types.StrictInterface.html +1 -1
  204. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  205. package/docs/types/types.UpdateableProperties.html +1 -1
  206. package/docs/types/types.ValidationType.html +1 -1
  207. package/docs/types/types.ViewModel.html +2 -2
  208. package/docs/types/types.ViewModelClass.html +1 -1
  209. package/docs/types/types.WeekdayName.html +1 -1
  210. package/docs/types/types.WhereStatementForDream.html +1 -1
  211. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  212. package/docs/variables/index.DreamConst.html +1 -1
  213. package/docs/variables/index.ops.html +26 -4
  214. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  215. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  216. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  217. package/docs/variables/system.primaryKeyTypes.html +1 -1
  218. package/package.json +1 -1
  219. package/dist/cjs/src/helpers/sspawn.js +0 -44
  220. package/dist/esm/src/helpers/sspawn.js +0 -44
  221. package/dist/types/src/helpers/sspawn.d.ts +0 -7
@@ -3,9 +3,13 @@ import DreamApp from '../dream-app/index.js';
3
3
  import Query from '../dream/Query.js';
4
4
  import DBClassDeprecation from '../helpers/cli/DBClassDeprecation.js';
5
5
  import generateDream from '../helpers/cli/generateDream.js';
6
- import sspawn from '../helpers/sspawn.js';
6
+ import EnvInternal from '../helpers/EnvInternal.js';
7
7
  export default class DreamBin {
8
8
  static async sync(onSync, options) {
9
+ if (!EnvInternal.isTest) {
10
+ DreamCLI.logger.log(`skipping sync: auto-generated type/schema files are only built when NODE_ENV=test (current NODE_ENV: ${process.env.NODE_ENV ?? 'unset'}). Run with NODE_ENV=test to regenerate.`);
11
+ return;
12
+ }
9
13
  const dreamApp = DreamApp.getOrFail();
10
14
  for (const connectionName of Object.keys(dreamApp.dbCredentials)) {
11
15
  await Query.dbDriverClass(connectionName).sync(connectionName, onSync, options);
@@ -76,7 +80,18 @@ export default class DreamBin {
76
80
  // to use it to generate docs for their apps.
77
81
  static async buildDocs() {
78
82
  DreamCLI.logger.logStartProgress('generating docs...');
79
- await sspawn('pnpm typedoc src/package-exports/*.ts --tsconfig ./tsconfig.esm.build.json --out docs');
83
+ // safe (R-015): all argv elements are constant literals; typedoc itself
84
+ // expands the glob so we don't need shell-form invocation.
85
+ await DreamCLI.spawn('pnpm', {
86
+ args: [
87
+ 'typedoc',
88
+ 'src/package-exports/*.ts',
89
+ '--tsconfig',
90
+ './tsconfig.esm.build.json',
91
+ '--out',
92
+ 'docs',
93
+ ],
94
+ });
80
95
  DreamCLI.logger.logEndProgress();
81
96
  }
82
97
  }
@@ -1,11 +1,12 @@
1
+ import { spawn } from 'child_process';
1
2
  import { InvalidArgumentError, Option } from 'commander';
2
3
  import DreamBin from '../bin/index.js';
3
4
  import DreamApp from '../dream-app/index.js';
4
5
  import Encrypt from '../encrypt/index.js';
6
+ import SspawnRequiresDevelopmentOrTest from '../errors/SspawnRequiresDevelopmentOrTest.js';
5
7
  import generateDream from '../helpers/cli/generateDream.js';
6
8
  import EnvInternal from '../helpers/EnvInternal.js';
7
9
  import loadRepl from '../helpers/loadRepl.js';
8
- import sspawn from '../helpers/sspawn.js';
9
10
  import DreamCliLogger from './logger/DreamCliLogger.js';
10
11
  import colorize from './logger/loggable/colorize.js';
11
12
  export const CLI_INDENT = ' ';
@@ -208,6 +209,8 @@ ${INDENT} Settings/CommunicationPreferences # src/app/models/Settings/Communi
208
209
  .alias('g:sti-child')
209
210
  .description(`Generates an STI (Single Table Inheritance) child model that extends an existing parent model. The child shares the parent's database table (discriminated by the \`type\` column) and can add child-specific columns. Generates a child model decorated with @STI(Parent), child serializers extending the parent's base serializers, a migration that ALTERs the parent table (not a new table), check constraints, a factory, and spec skeleton.
210
211
  ${INDENT}
212
+ ${INDENT}If the child declares no additional columns, only the model file is generated — no migration is created. STI children share the parent's table, so a no-columns child requires no schema change. Add a migration only by passing positional field:type args, in which case the generator emits one with the appropriate check constraint. STI children never receive @SoftDelete() — soft delete is enforced at the parent level only — and the generator does not accept --no-soft-delete.
213
+ ${INDENT}
211
214
  ${INDENT}The parent must already exist (typically generated with g:model --sti-base-serializer or g:resource --sti-base-serializer).
212
215
  ${INDENT}
213
216
  ${INDENT}Examples:
@@ -288,7 +291,7 @@ ${INDENT} pnpm psy g:encryption-key --algorithm=aes-128-gcm`)
288
291
  });
289
292
  program
290
293
  .command('db:migrate')
291
- .description(`Runs all pending database migrations in order, then automatically syncs types (in development/test). This is the primary command for applying schema changes after generating or editing a migration.
294
+ .description(`Runs all pending database migrations in order, then automatically syncs types (only when NODE_ENV=test, to avoid clobbering generated types from a stale dev database). This is the primary command for applying schema changes after generating or editing a migration.
292
295
  ${INDENT}
293
296
  ${INDENT}Example workflow:
294
297
  ${INDENT} pnpm psy g:migration add-phone-to-users phone:string:optional
@@ -298,14 +301,14 @@ ${INDENT} pnpm psy db:migrate`)
298
301
  .action(async ({ skipSync }) => {
299
302
  await initializeDreamApp({ bypassDreamIntegrityChecks: true });
300
303
  await DreamBin.dbMigrate();
301
- if (EnvInternal.isDevelopmentOrTest && !skipSync) {
304
+ if (EnvInternal.isTest && !skipSync) {
302
305
  await DreamBin.sync(onSync);
303
306
  }
304
307
  process.exit();
305
308
  });
306
309
  program
307
310
  .command('db:rollback')
308
- .description(`Rolls back the most recent migration(s), then automatically syncs types (in development/test). Use this to undo a migration so you can edit and re-run it.
311
+ .description(`Rolls back the most recent migration(s), then automatically syncs types (only when NODE_ENV=test, to avoid clobbering generated types from a stale dev database). Use this to undo a migration so you can edit and re-run it.
309
312
  ${INDENT}
310
313
  ${INDENT}Examples:
311
314
  ${INDENT} pnpm psy db:rollback # rolls back the last migration
@@ -315,7 +318,7 @@ ${INDENT} pnpm psy db:rollback --steps=3 # rolls back the last 3 migrations`
315
318
  .action(async ({ steps, skipSync }) => {
316
319
  await initializeDreamApp({ bypassDreamIntegrityChecks: true });
317
320
  await DreamBin.dbRollback({ steps });
318
- if (EnvInternal.isDevelopmentOrTest && !skipSync) {
321
+ if (EnvInternal.isTest && !skipSync) {
319
322
  await DreamBin.sync(onSync);
320
323
  }
321
324
  process.exit();
@@ -409,15 +412,84 @@ ${INDENT}Examples: User, Place, Room/Bedroom, Settings/CommunicationPreferences`
409
412
  process.exit();
410
413
  });
411
414
  }
412
- /*
413
- * the default spawn provided by node:child_process is incompatible
414
- * with promises. this will automatically wrap the spawn method,
415
- * and will by default connect STDOUT to the current STDOUT,
416
- * so that whatever the command is outputting is output to the
417
- * primary STDOUT context.
415
+ /**
416
+ * Run a developer-authored CLI command. Always runs in argv form (the
417
+ * underlying child_process `spawn` is called with `shell: false`):
418
+ * `command` is exec'd literally and `opts.args` are passed as separate
419
+ * argv elements. Shell-form invocation is intentionally not supported
420
+ * there is no caller that needs `&&`-chaining, globs, or other shell
421
+ * features that can't be expressed as argv.
422
+ *
423
+ * For backward compatibility, `command` may contain implicit args
424
+ * separated by whitespace (e.g. `'pnpm psy sync'`); the leading token
425
+ * becomes the program and the rest are split out and prepended to any
426
+ * `opts.args` so the original argument order is preserved:
427
+ *
428
+ * DreamCLI.spawn('pnpm psy sync')
429
+ * → spawn('pnpm', ['psy', 'sync'])
430
+ *
431
+ * DreamCLI.spawn('pnpm psy', { args: ['sync', '--flag'] })
432
+ * → spawn('pnpm', ['psy', 'sync', '--flag'])
433
+ *
434
+ * ## Threat model (R-015)
435
+ *
436
+ * For dev-time CLI glue only (scaffolding, doc generation, type sync).
437
+ * **No runtime HTTP request input ever reaches this function.** Inputs
438
+ * are constant literals or composed from developer-supplied config
439
+ * (package.json scripts, CLI argv, scaffold templates) — never from
440
+ * runtime request input or any other untrusted external source.
441
+ *
442
+ * Argv-form is the safe choice for any caller that interpolates a
443
+ * config value, path, or credential: a database password containing
444
+ * `$` or backticks is passed literally to the child rather than
445
+ * interpreted by a shell.
446
+ *
447
+ * ## Layered defense
448
+ *
449
+ * Primary gate: every caller restricts spawn use to dev/test code paths
450
+ * (CLI commands, the dev watcher, scaffold-time code generators,
451
+ * generated `cli:sync` initializers wrapped in
452
+ * `if (AppEnv.isDevelopmentOrTest)`).
453
+ *
454
+ * Backstop: throws `SspawnRequiresDevelopmentOrTest` when `NODE_ENV` is
455
+ * anything other than `development` or `test`. Checking
456
+ * `!isDevelopmentOrTest` (rather than `isProduction`) means staging-style
457
+ * envs and any unforeseen NODE_ENV value also fail closed.
418
458
  */
419
459
  static async spawn(command, opts) {
420
- return await sspawn(command, opts);
460
+ const tokens = command.trim().split(/\s+/).filter(Boolean);
461
+ const [program = '', ...implicitArgs] = tokens;
462
+ const { args: callerArgs = [], onStdout, ...spawnOpts } = opts ?? {};
463
+ const args = [...implicitArgs, ...callerArgs];
464
+ if (!EnvInternal.isDevelopmentOrTest) {
465
+ throw new SspawnRequiresDevelopmentOrTest([program, ...args].join(' '));
466
+ }
467
+ return new Promise((accept, reject) => {
468
+ const proc = spawn(program, args, { shell: false, ...spawnOpts });
469
+ // NOTE: stdout spy so this CLI utility can hijack the stdout from the
470
+ // child command and route it through a caller-provided sink.
471
+ proc.stdout?.on('data', chunk => {
472
+ const txt = chunk?.toString()?.trim();
473
+ if (typeof txt !== 'string' || !txt)
474
+ return;
475
+ if (onStdout) {
476
+ onStdout(txt);
477
+ }
478
+ else {
479
+ // eslint-disable-next-line no-console
480
+ console.log(txt);
481
+ }
482
+ });
483
+ proc.stdout?.on('error', handleSpawnError);
484
+ proc.stderr?.on('error', handleSpawnError);
485
+ proc.stderr?.on('data', handleSpawnError);
486
+ proc.on('error', handleSpawnError);
487
+ proc.on('close', code => {
488
+ if (code !== 0)
489
+ reject(code);
490
+ accept();
491
+ });
492
+ });
421
493
  }
422
494
  static get logger() {
423
495
  this._logger ||= new DreamCliLogger();
@@ -432,3 +504,7 @@ function myParseInt(value) {
432
504
  }
433
505
  return parsedValue;
434
506
  }
507
+ function handleSpawnError(err) {
508
+ // eslint-disable-next-line no-console
509
+ console.error(err?.toString());
510
+ }
@@ -5,7 +5,6 @@ import colorize from '../../cli/logger/loggable/colorize.js';
5
5
  import DreamApp from '../../dream-app/index.js';
6
6
  import ASTKyselyCodegenEnhancer from '../../helpers/cli/ASTKyselyCodegenEnhancer.js';
7
7
  import dreamPath from '../../helpers/path/dreamPath.js';
8
- import sspawn from '../../helpers/sspawn.js';
9
8
  import dbTypesFilenameForConnection from './dbTypesFilenameForConnection.js';
10
9
  export default async function syncDbTypesFiles(connectionName) {
11
10
  const dreamApp = DreamApp.getOrFail();
@@ -16,17 +15,23 @@ export default async function syncDbTypesFiles(connectionName) {
16
15
  const absoluteDbSyncPath = path.join(dreamApp.projectRoot, dbSyncFilePath);
17
16
  await CliFileWriter.cache(absoluteDbSyncPath);
18
17
  const lowLevelDbOpts = dreamApp.dbCredentialsFor(connectionName);
19
- const dialect = `--dialect=${driverClass.syncDialect}`;
20
- const url = `--url=${driverClass.syncDialect}://${dbConf.user}${dbConf.password ? `:${dbConf.password}` : ''}@${dbConf.host}:${dbConf.port}/${dbConf.name}`;
21
- const outfile = `--out-file=${absoluteDbSyncPath}`;
22
- const includePattern = lowLevelDbOpts?.tableIncludePattern
23
- ? `--include-pattern="${lowLevelDbOpts.tableIncludePattern}"`
24
- : '';
25
- const excludePattern = lowLevelDbOpts?.tableExcludePattern
26
- ? `--exclude-pattern="${lowLevelDbOpts.tableExcludePattern}"`
27
- : '';
28
- const kyselyCodegenCmd = `kysely-codegen ${dialect} ${url} ${includePattern} ${excludePattern} ${outfile}`;
29
- await sspawn(kyselyCodegenCmd, {
18
+ // Argv form (R-015): the connection URL embeds the DB password, and passwords
19
+ // may legitimately contain shell meta-characters (`$`, backticks, spaces, etc.).
20
+ // DreamCLI.spawn always uses shell:false, so each arg is passed literally to
21
+ // the child rather than parsed by a shell — credentials are preserved and any
22
+ // future mutable source can't open a command-injection surface.
23
+ const userinfo = `${dbConf.user}${dbConf.password ? `:${dbConf.password}` : ''}`;
24
+ const url = `${driverClass.syncDialect}://${userinfo}@${dbConf.host}:${dbConf.port}/${dbConf.name}`;
25
+ const args = [`--dialect=${driverClass.syncDialect}`, `--url=${url}`];
26
+ if (lowLevelDbOpts?.tableIncludePattern) {
27
+ args.push(`--include-pattern=${lowLevelDbOpts.tableIncludePattern}`);
28
+ }
29
+ if (lowLevelDbOpts?.tableExcludePattern) {
30
+ args.push(`--exclude-pattern=${lowLevelDbOpts.tableExcludePattern}`);
31
+ }
32
+ args.push(`--out-file=${absoluteDbSyncPath}`);
33
+ await DreamCLI.spawn('kysely-codegen', {
34
+ args,
30
35
  onStdout: message => {
31
36
  DreamCLI.logger.logContinueProgress(colorize(`[db]`, { color: 'greenBright' }) + ' ' + message, {
32
37
  logPrefixColor: 'greenBright',
@@ -99,7 +99,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
99
99
  database: DreamApp.getOrFail().dbName(connectionName, dbConnectionType),
100
100
  host: connectionConf.host || 'localhost',
101
101
  port: connectionConf.port || 5432,
102
- ssl: connectionConf.useSsl ? defaultPostgresSslConfig(connectionConf) : false,
102
+ ssl: resolvePostgresSsl(connectionConf),
103
103
  }),
104
104
  });
105
105
  }
@@ -2043,15 +2043,26 @@ const associationStringToAssociationAndMaybeAlias = function ({ dreamClass, asso
2043
2043
  alias,
2044
2044
  };
2045
2045
  };
2046
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
2047
- function defaultPostgresSslConfig(connectionConf) {
2048
- // TODO: properly configure (https://rvohealth.atlassian.net/browse/PDTC-2914)
2049
- return {
2050
- rejectUnauthorized: false,
2051
- // ca: fs.readFileSync('/path/to/server-certificates/root.crt').toString(),
2052
- // key: fs.readFileSync('/path/to/client-key/postgresql.key').toString(),
2053
- // cert: fs.readFileSync('/path/to/client-certificates/postgresql.crt').toString(),
2054
- };
2046
+ /**
2047
+ * Resolve the value passed to `pg.Pool`'s `ssl` field for a given credential.
2048
+ *
2049
+ * Precedence:
2050
+ * 1. If `connectionConf.ssl` is set (boolean or `tls.ConnectionOptions`),
2051
+ * pass it straight through to `pg`. This is the verified-TLS path —
2052
+ * apps configure `{ rejectUnauthorized: true, ca: <bundle> }` for
2053
+ * authenticated TLS against a private PKI, or `true` to use Node's
2054
+ * default verification against the system CA store.
2055
+ * 2. Else if `connectionConf.useSsl` is `true`, fall back to
2056
+ * `{ rejectUnauthorized: false }` — encrypted but **not** authenticated.
2057
+ * Preserved for back-compat; new code should set `ssl` explicitly.
2058
+ * 3. Else disable TLS.
2059
+ */
2060
+ export function resolvePostgresSsl(connectionConf) {
2061
+ if (connectionConf.ssl !== undefined)
2062
+ return connectionConf.ssl;
2063
+ if (connectionConf.useSsl)
2064
+ return { rejectUnauthorized: false };
2065
+ return false;
2055
2066
  }
2056
2067
  function shouldSoftDelete(dream, reallyDestroy) {
2057
2068
  const dreamClass = dream.constructor;
@@ -1,11 +1,24 @@
1
1
  import * as crypto from 'crypto';
2
+ import DecryptionError from '../../../errors/encrypt/DecryptionError.js';
3
+ import DecryptionParseError from '../../../errors/encrypt/DecryptionParseError.js';
2
4
  export default function decryptAESGCM(algorithm, encrypted, key) {
3
- const { ciphertext, tag, iv } = unpackPayloadOrFail(encrypted);
4
- const decipher = crypto.createDecipheriv(algorithm, Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'));
5
- decipher.setAuthTag(Buffer.from(tag, 'base64'));
6
- let plaintext = decipher.update(ciphertext, 'base64', 'utf8');
7
- plaintext += decipher.final('utf8');
8
- return JSON.parse(plaintext);
5
+ let plaintext;
6
+ try {
7
+ const { ciphertext, tag, iv } = unpackPayloadOrFail(encrypted);
8
+ const decipher = crypto.createDecipheriv(algorithm, Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'));
9
+ decipher.setAuthTag(Buffer.from(tag, 'base64'));
10
+ plaintext = decipher.update(ciphertext, 'base64', 'utf8');
11
+ plaintext += decipher.final('utf8');
12
+ }
13
+ catch (cause) {
14
+ throw Object.assign(new DecryptionError(), { cause });
15
+ }
16
+ try {
17
+ return JSON.parse(plaintext);
18
+ }
19
+ catch (cause) {
20
+ throw Object.assign(new DecryptionParseError(), { cause });
21
+ }
9
22
  }
10
23
  function unpackPayloadOrFail(payload) {
11
24
  const unpackedPayload = (typeof payload === 'string' ? JSON.parse(Buffer.from(payload, 'base64').toString()) : payload);
@@ -1,3 +1,5 @@
1
+ import DecryptionError from '../errors/encrypt/DecryptionError.js';
2
+ import DecryptionWithRotationError from '../errors/encrypt/DecryptionWithRotationError.js';
1
3
  import MissingEncryptionKey from '../errors/encrypt/MissingEncryptionKey.js';
2
4
  import decryptAESGCM from './algorithms/aes-gcm/decryptAESGCM.js';
3
5
  import encryptAESGCM from './algorithms/aes-gcm/encryptAESGCM.js';
@@ -20,6 +22,30 @@ export default class Encrypt {
20
22
  }
21
23
  }
22
24
  }
25
+ /**
26
+ * Decrypts a value previously produced by {@link Encrypt.encrypt}.
27
+ *
28
+ * Behavior depends on whether `legacyOpts` is provided:
29
+ *
30
+ * **Two-arg form** (no rotation):
31
+ * - `null`/`undefined` input returns `null`.
32
+ * - Cipher op / auth tag / payload-shape failure throws `DecryptionError`.
33
+ * - Successful decrypt with non-JSON plaintext throws `DecryptionParseError`.
34
+ *
35
+ * **Three-arg form** (rotation): tries the current key first; on
36
+ * `DecryptionError` falls back to the legacy key. If both fail, throws
37
+ * `DecryptionWithRotationError` carrying both per-key errors. A
38
+ * `DecryptionParseError` from the current key is **not** retried — the
39
+ * cipher already matched, so a parse failure means the encrypted format
40
+ * is wrong (an app bug), not a wrong key.
41
+ *
42
+ * `MissingEncryptionKey` propagates from either form when a key is missing.
43
+ *
44
+ * @throws MissingEncryptionKey
45
+ * @throws DecryptionError
46
+ * @throws DecryptionParseError
47
+ * @throws DecryptionWithRotationError
48
+ */
23
49
  static decrypt(encrypted, { algorithm, key }, legacyOpts) {
24
50
  if (legacyOpts)
25
51
  return this.attemptDecryptionWithLegacyKeys(encrypted, { algorithm, key }, legacyOpts);
@@ -31,12 +57,7 @@ export default class Encrypt {
31
57
  case 'aes-256-gcm':
32
58
  case 'aes-192-gcm':
33
59
  case 'aes-128-gcm':
34
- try {
35
- return decryptAESGCM(algorithm, encrypted, key);
36
- }
37
- catch {
38
- return null;
39
- }
60
+ return decryptAESGCM(algorithm, encrypted, key);
40
61
  default: {
41
62
  // protection so that if a new EncryptAlgorithm is ever added, this will throw a type error at build time
42
63
  const _never = algorithm;
@@ -44,12 +65,60 @@ export default class Encrypt {
44
65
  }
45
66
  }
46
67
  }
47
- static attemptDecryptionWithLegacyKeys(encrypted, { algorithm, key }, legacyOpts) {
48
- const decrypted = this.decrypt(encrypted, { algorithm, key });
49
- if (decrypted !== null)
50
- return decrypted;
51
- return this.decrypt(encrypted, legacyOpts);
68
+ static attemptDecryptionWithLegacyKeys(encrypted, currentOpts, legacyOpts) {
69
+ let currentKeyError;
70
+ try {
71
+ return this.decrypt(encrypted, currentOpts);
72
+ }
73
+ catch (err) {
74
+ if (!(err instanceof DecryptionError))
75
+ throw err;
76
+ currentKeyError = err;
77
+ }
78
+ try {
79
+ return this.decrypt(encrypted, legacyOpts);
80
+ }
81
+ catch (err) {
82
+ if (!(err instanceof DecryptionError))
83
+ throw err;
84
+ throw new DecryptionWithRotationError(currentKeyError, err);
85
+ }
52
86
  }
87
+ /**
88
+ * Generates a base64-encoded random key suitable for the given algorithm.
89
+ *
90
+ * ## Rotation workflow
91
+ *
92
+ * 1. Generate a new key: `const newKey = Encrypt.generateKey('aes-256-gcm')`.
93
+ * 2. Configure rotation by setting both `current` and `legacy`. For
94
+ * encrypted columns: `dreamApp.set('encryption', { columns: { current:
95
+ * { algorithm: 'aes-256-gcm', key: newKey }, legacy: { algorithm:
96
+ * 'aes-256-gcm', key: oldKey } } })`. For cookies, use the equivalent
97
+ * shape under `psychicApp.set('encryption', { cookies: { current,
98
+ * legacy } })`.
99
+ * 3. Deploy. New encryptions use `current`; existing ciphertext continues
100
+ * to decrypt via `legacy` fallback.
101
+ * 4. For cookies, wait at least the cookie `maxAge` so all in-flight
102
+ * cookies have either expired or been re-issued under the new key. For
103
+ * `@Encrypted` columns, re-encrypt every existing row under the new
104
+ * key (read each row and write it back; the setter re-encrypts with
105
+ * `current`).
106
+ * 5. Drop `legacy` from config and deploy again.
107
+ *
108
+ * ## When to rotate
109
+ *
110
+ * - On a scheduled cadence (90–180 days is a reasonable policy default).
111
+ * - Incident response: leaked env file, departing employee with key
112
+ * access, or any suspected key compromise.
113
+ *
114
+ * ## How long to keep `legacy`
115
+ *
116
+ * - For cookies: at least the cookie `maxAge`, so in-flight sessions are
117
+ * not forced to re-authenticate.
118
+ * - For `@Encrypted` columns: until every existing row has been
119
+ * re-encrypted under the new key. Dropping `legacy` early will cause
120
+ * `DecryptionWithRotationError` on any not-yet-rewritten row.
121
+ */
53
122
  static generateKey(algorithm) {
54
123
  switch (algorithm) {
55
124
  case 'aes-256-gcm':
@@ -0,0 +1,26 @@
1
+ export default class SspawnRequiresDevelopmentOrTest extends Error {
2
+ command;
3
+ constructor(command) {
4
+ super();
5
+ this.command = command;
6
+ }
7
+ get message() {
8
+ return `
9
+ DreamCLI.spawn refused to run outside development or test
10
+ (NODE_ENV must be 'development' or 'test').
11
+
12
+ DreamCLI.spawn is dev-time CLI glue (scaffolding, docs generation,
13
+ type sync). It must never run in a deployed process — production has
14
+ no business shelling out arbitrary commands, and refusing here turns
15
+ the dev-only contract into a runtime invariant. Checking
16
+ \`!isDevelopmentOrTest\` (rather than \`isProduction\`) means
17
+ staging-style envs and any unforeseen NODE_ENV value also fail closed.
18
+
19
+ If you reached this from a deploy step, route the work through direct
20
+ child_process APIs at the call site with explicit argv and a documented
21
+ threat model rather than through this helper.
22
+
23
+ command: ${this.command}
24
+ `;
25
+ }
26
+ }
@@ -0,0 +1,5 @@
1
+ export default class DecryptionError extends Error {
2
+ get message() {
3
+ return 'Failed to decrypt: cipher operation, authentication tag, or payload shape was invalid. The ciphertext may have been tampered with, corrupted, or encrypted with a different key.';
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ export default class DecryptionParseError extends Error {
2
+ get message() {
3
+ return 'Decryption succeeded but the plaintext was not valid JSON. This indicates an encrypted-format mismatch (e.g. a non-JSON value was passed into Encrypt.encrypt), not tampering.';
4
+ }
5
+ }
@@ -0,0 +1,12 @@
1
+ export default class DecryptionWithRotationError extends Error {
2
+ currentKeyError;
3
+ legacyKeyError;
4
+ constructor(currentKeyError, legacyKeyError) {
5
+ super(undefined, { cause: { current: currentKeyError, legacy: legacyKeyError } });
6
+ this.currentKeyError = currentKeyError;
7
+ this.legacyKeyError = legacyKeyError;
8
+ }
9
+ get message() {
10
+ return 'Failed to decrypt with both the current and legacy keys. The ciphertext may have been tampered with, corrupted, or encrypted with a key that is no longer in rotation.';
11
+ }
12
+ }
@@ -95,7 +95,8 @@ export default function generateMigrationContent({ connectionName = 'default', t
95
95
  columnDefs.push(generateBooleanStr(attributeName, { omitInlineNonNull }));
96
96
  break;
97
97
  case 'encrypted':
98
- columnDefs.push(generateColumnStr(`encrypted_${attributeName}`, 'text', descriptors, {
98
+ attributeName = `encrypted_${attributeName}`;
99
+ columnDefs.push(generateColumnStr(attributeName, 'text', descriptors, {
99
100
  omitInlineNonNull,
100
101
  }));
101
102
  break;
@@ -63,17 +63,38 @@ const ops = {
63
63
  * Creates an `OpsStatement` using the SQL `LIKE` operator for case-sensitive pattern matching.
64
64
  * Use `%` as a wildcard in the pattern string.
65
65
  *
66
+ * **User-supplied patterns:** the value is bound as a parameter (no SQL
67
+ * injection), but `%`, `_`, and `\` in the bound value are still interpreted
68
+ * as wildcards by the SQL engine. Wrap user input with `ops.like.escape`
69
+ * if you need literal matching:
70
+ *
71
+ * User.where({ name: ops.like(`%${ops.like.escape(query)}%`) })
72
+ *
73
+ * `ops.like.escape` escapes the three metacharacters that PostgreSQL's
74
+ * `LIKE` / `ILIKE` operators treat as wildcards (`\`, `%`, `_`) so the
75
+ * returned string matches literally. The same helper applies to all four
76
+ * LIKE-family ops (`like`, `ilike`, `not.like`, `not.ilike`); it is exposed
77
+ * here as the canonical entry point. If a caller uses `ESCAPE '...'` with
78
+ * a non-default character this helper will not be appropriate.
79
+ *
66
80
  * @param like - The pattern string (e.g. `'%hello%'`).
67
81
  * @returns An `OpsStatement` using the `like` operator.
68
82
  *
69
83
  * @example
70
84
  * User.where({ name: ops.like('%alice%') })
71
85
  */
72
- like: (like) => new OpsStatement('like', like),
86
+ like: Object.assign((like) => new OpsStatement('like', like), {
87
+ escape: (input) => input.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_'),
88
+ }),
73
89
  /**
74
90
  * Creates an `OpsStatement` using the SQL `ILIKE` operator for case-insensitive pattern matching.
75
91
  * Use `%` as a wildcard in the pattern string.
76
92
  *
93
+ * **User-supplied patterns:** the value is bound as a parameter (no SQL
94
+ * injection), but `%`, `_`, and `\` in the bound value are still interpreted
95
+ * as wildcards by the SQL engine. Wrap user input with `ops.like.escape`
96
+ * if you need literal matching.
97
+ *
77
98
  * @param ilike - The pattern string (e.g. `'%hello%'`).
78
99
  * @returns An `OpsStatement` using the `ilike` operator.
79
100
  *
@@ -201,6 +222,9 @@ const ops = {
201
222
  /**
202
223
  * Creates an `OpsStatement` using the `NOT LIKE` operator for case-sensitive pattern exclusion.
203
224
  *
225
+ * **User-supplied patterns:** see the note on `ops.like`. Wrap user input
226
+ * with `ops.like.escape` for literal matching.
227
+ *
204
228
  * @param like - The pattern string (e.g. `'%spam%'`).
205
229
  * @returns An `OpsStatement` using the `not like` operator.
206
230
  *
@@ -211,6 +235,9 @@ const ops = {
211
235
  /**
212
236
  * Creates an `OpsStatement` using the `NOT ILIKE` operator for case-insensitive pattern exclusion.
213
237
  *
238
+ * **User-supplied patterns:** see the note on `ops.ilike`. Wrap user input
239
+ * with `ops.like.escape` for literal matching.
240
+ *
214
241
  * @param ilike - The pattern string (e.g. `'%spam%'`).
215
242
  * @returns An `OpsStatement` using the `not ilike` operator.
216
243
  *
@@ -20,14 +20,27 @@ export default class DreamSerializerBuilder {
20
20
  /**
21
21
  * Includes an attribute from a nested object in the serialized output.
22
22
  *
23
- * Accesses `targetName.name` on the data object. If the target object
24
- * or the delegated attribute is null/undefined, the `default` option value
25
- * will be used if provided.
23
+ * Accesses `targetName.name` on the data object. When the target object or the
24
+ * delegated attribute is null/undefined, the resolution order is:
25
+ * 1. `default` if provided renders the default value
26
+ * 2. `required: false` → omits the key entirely from the rendered output
27
+ * 3. otherwise → renders `null`
26
28
  *
27
29
  * When the target is a Dream model, OpenAPI types may be automatically inferred
28
30
  * for standard database columns. For json/jsonb columns or non-Dream targets,
29
31
  * the `openapi` option is required.
30
32
  *
33
+ * `optional` and `required` are not aliases — they encode different things and can
34
+ * be used together:
35
+ * - `optional: true` is an OpenAPI-only marker (no runtime effect). It declares
36
+ * that the rendered value may be `null` (e.g. when delegating through a HasOne
37
+ * that may be missing). Use this when you want the key to always be present
38
+ * but the value to be nullable in the schema.
39
+ * - `required: false` affects both runtime and OpenAPI. At runtime, when the
40
+ * resolved value is `undefined` (which includes the case of a missing delegated
41
+ * association with no `default`), the key is omitted from the rendered output.
42
+ * In OpenAPI, the field is marked as not required.
43
+ *
31
44
  * @param targetName - The property name containing the target object (e.g., an association name)
32
45
  * @param name - The attribute name within the target object
33
46
  * @param options - Configuration options:
@@ -35,16 +48,22 @@ export default class DreamSerializerBuilder {
35
48
  * (e.g., delegating `'user', 'email'` with `as: 'userEmail'` outputs the value
36
49
  * under `userEmail`)
37
50
  * - `default` - Value to use when the target object or its attribute is null/undefined
51
+ * (not available when delegating to a `'type'` STI discriminator column: substituting
52
+ * a discriminator string when the association is actually missing produces a response
53
+ * indistinguishable from "association present with that type," which is misleading)
38
54
  * - `openapi` - OpenAPI schema definition; required for non-Dream targets and json/jsonb
39
55
  * columns, optional for standard Dream columns (where types are inferred)
40
- * - `optional` - Set to `true` to indicate the value can be null in the OpenAPI schema
41
- * (wraps the type in `anyOf: [schema, { type: 'null' }]`). For Dream models, this is
42
- * auto-inferred from optional BelongsTo associations. Use this when delegating through
43
- * a HasOne or other nullable association.
56
+ * - `optional` - Set to `true` to mark the value as nullable in the OpenAPI schema
57
+ * (wraps the type in `anyOf: [schema, { type: 'null' }]`). OpenAPI-only the
58
+ * key is still rendered (as `null`). For Dream models, this is auto-inferred
59
+ * from optional BelongsTo associations. Use this when delegating through a
60
+ * HasOne or other nullable association.
44
61
  * - `precision` - Round decimal values to the specified number of decimal places (0–9)
45
- * during rendering; does not affect the OpenAPI shape
46
- * - `required` - Set to `false` to mark the attribute as optional in the OpenAPI schema;
47
- * when omitted, attributes are required by default
62
+ * during rendering; does not affect the OpenAPI shape (not available when delegating
63
+ * to a `'type'` STI discriminator column, which is always a string enum)
64
+ * - `required` - Set to `false` to omit the key from the rendered output when the
65
+ * resolved value is `undefined` (including a missing delegated association with
66
+ * no `default`), and to mark the field as not required in the OpenAPI schema
48
67
  * @returns The serializer builder for method chaining
49
68
  *
50
69
  * @example