effect-app 4.0.0-beta.211 → 4.0.0-beta.213

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Schema/ext.ts CHANGED
@@ -1,5 +1,35 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  /* eslint-disable @typescript-eslint/no-unsafe-return */
3
+ /**
4
+ * # `withConstructorDefault` policy
5
+ *
6
+ * The `withConstructorDefault` properties exported throughout this module
7
+ * (and from `numbers.ts`, `moreStrings.ts`, `ids.ts`) attach a default value
8
+ * that is **only** applied during construction — i.e. when the field is
9
+ * omitted from the input to a Schema constructor / `.make(...)` call.
10
+ *
11
+ * They are **NOT** applied during `decode` (JSON, database rows, RPC payloads,
12
+ * etc.). Decoding a payload with a missing field will still fail with a parse
13
+ * error, exactly as if the default were not present.
14
+ *
15
+ * Concretely this means `withConstructorDefault` MUST NOT be relied on as a
16
+ * just-in-time migration mechanism for database fields. If a stored record is
17
+ * missing a newly added field, the constructor default will not fill it in on
18
+ * read — decoding will fail.
19
+ *
20
+ * ## Don't reach for `withDecodingDefault*` either
21
+ *
22
+ * The sibling `withDecodingDefaultType` (and `withDecodingDefault`) extensions
23
+ * exist, but they are discouraged for migrating persisted data. A missing
24
+ * field in a stored record is just as likely to be data corruption as it is
25
+ * an old-shape document; silently substituting a default hides the problem
26
+ * and can poison downstream aggregates.
27
+ *
28
+ * Prefer an **explicit, preferably versioned** migration of database data
29
+ * (a schema-version field, a one-shot backfill, or a transform on read that
30
+ * is gated on an explicit version marker) over shoving missing fields under
31
+ * the rug with a decode-time default.
32
+ */
3
33
  import { Config, Effect, Function, Option, pipe, type SchemaAST, SchemaIssue, SchemaTransformation } from "effect"
4
34
  import * as S from "effect/Schema"
5
35
  import { isDateValid } from "effect/Schema"
@@ -75,78 +105,163 @@ export interface DateFromString extends S.decodeTo<S.Date, S.String> {}
75
105
  */
76
106
  export const DateFromString: DateFromString = DateString.pipe(S.decodeTo(S.Date, SchemaTransformation.dateFromString))
77
107
 
78
- /**
79
- * Like the default Schema `Date` but from String with `withDefault` => now
80
- */
108
+ /** Like the default Schema `Date` but from String, with default helpers. */
81
109
  export const Date = Object.assign(DateFromString, {
82
- withDefault: DateFromString.pipe(S.withConstructorDefault(Effect.sync(() => new global.Date()))),
110
+ /**
111
+ * Construction-only default `new Date()`. Applied only when the field is
112
+ * omitted from `.make(...)` input. NOT applied during decode — cannot be
113
+ * used to JIT-migrate database fields. See file-level note.
114
+ */
115
+ withConstructorDefault: DateFromString.pipe(S.withConstructorDefault(Effect.sync(() => new global.Date()))),
116
+ /**
117
+ * Decode-time default `new Date()`. **Discouraged for persisted data:** a
118
+ * missing field may be data corruption, not an old-shape document; silently
119
+ * substituting `new Date()` hides the problem. Prefer an explicit,
120
+ * preferably versioned migration over a decode-time fallback. See
121
+ * file-level note.
122
+ */
83
123
  withDecodingDefaultType: DateFromString.pipe(S.withDecodingDefaultType(Effect.sync(() => new global.Date())))
84
124
  })
85
125
 
86
- /**
87
- * Like the default Schema `DateValid` but from String with `withDefault` => now
88
- */
126
+ /** Like the default Schema `DateValid` but from String, with default helpers. */
89
127
  export const DateValid = Object.assign(Date.check(isDateValid()), {
90
- withDefault: DateFromString.pipe(S.withConstructorDefault(Effect.sync(() => new global.Date()))),
128
+ /**
129
+ * Construction-only default `new Date()`. Applied only when the field is
130
+ * omitted from `.make(...)` input. NOT applied during decode — cannot be
131
+ * used to JIT-migrate database fields. See file-level note.
132
+ */
133
+ withConstructorDefault: DateFromString.pipe(S.withConstructorDefault(Effect.sync(() => new global.Date()))),
134
+ /**
135
+ * Decode-time default `new Date()`. **Discouraged for persisted data:** a
136
+ * missing field may be data corruption, not an old-shape document; silently
137
+ * substituting `new Date()` hides the problem. Prefer an explicit,
138
+ * preferably versioned migration over a decode-time fallback. See
139
+ * file-level note.
140
+ */
91
141
  withDecodingDefaultType: DateFromString.pipe(S.withDecodingDefaultType(Effect.sync(() => new global.Date())))
92
142
  })
93
143
 
94
- /**
95
- * Like the default Schema `Boolean` but with `withDefault` => false
96
- */
144
+ /** Like the default Schema `Boolean` but with default helpers. */
97
145
  export const Boolean = Object.assign(S.Boolean, {
98
- withDefault: S.Boolean.pipe(S.withConstructorDefault(Effect.succeed(false))),
146
+ /**
147
+ * Construction-only default `false`. Applied only when the field is
148
+ * omitted from `.make(...)` input. NOT applied during decode — cannot be
149
+ * used to JIT-migrate database fields. See file-level note.
150
+ */
151
+ withConstructorDefault: S.Boolean.pipe(S.withConstructorDefault(Effect.succeed(false))),
152
+ /**
153
+ * Decode-time default `false`. **Discouraged for persisted data:** a
154
+ * missing field may be data corruption, not an old-shape document; silently
155
+ * substituting `false` hides the problem. Prefer an explicit, preferably
156
+ * versioned migration over a decode-time fallback. See file-level note.
157
+ */
99
158
  withDecodingDefaultType: S.Boolean.pipe(S.withDecodingDefaultType(Effect.succeed(false)))
100
159
  })
101
160
 
102
161
  /**
103
- * You probably want to use `Finite` instead of this.
104
- * Like the default Schema `Number` but with `withDefault` => 0
162
+ * You probably want to use `Finite` instead of this. Like the default Schema
163
+ * `Number` but with default helpers.
105
164
  */
106
165
  export const Number = Object.assign(S.Number, {
107
- withDefault: S.Number.pipe(S.withConstructorDefault(Effect.succeed(0))),
166
+ /**
167
+ * Construction-only default `0`. Applied only when the field is omitted
168
+ * from `.make(...)` input. NOT applied during decode — cannot be used to
169
+ * JIT-migrate database fields. See file-level note.
170
+ */
171
+ withConstructorDefault: S.Number.pipe(S.withConstructorDefault(Effect.succeed(0))),
172
+ /**
173
+ * Decode-time default `0`. **Discouraged for persisted data:** a missing
174
+ * field may be data corruption, not an old-shape document; silently
175
+ * substituting `0` hides the problem. Prefer an explicit, preferably
176
+ * versioned migration over a decode-time fallback. See file-level note.
177
+ */
108
178
  withDecodingDefaultType: S.Number.pipe(S.withDecodingDefaultType(Effect.succeed(0)))
109
179
  })
110
180
 
111
- /**
112
- * Like the default Schema `Finite` but with `withDefault` => 0
113
- */
181
+ /** Like the default Schema `Finite` but with default helpers. */
114
182
  export const Finite = Object.assign(S.Finite, {
115
- withDefault: S.Finite.pipe(S.withConstructorDefault(Effect.succeed(0))),
183
+ /**
184
+ * Construction-only default `0`. Applied only when the field is omitted
185
+ * from `.make(...)` input. NOT applied during decode — cannot be used to
186
+ * JIT-migrate database fields. See file-level note.
187
+ */
188
+ withConstructorDefault: S.Finite.pipe(S.withConstructorDefault(Effect.succeed(0))),
189
+ /**
190
+ * Decode-time default `0`. **Discouraged for persisted data:** a missing
191
+ * field may be data corruption, not an old-shape document; silently
192
+ * substituting `0` hides the problem. Prefer an explicit, preferably
193
+ * versioned migration over a decode-time fallback. See file-level note.
194
+ */
116
195
  withDecodingDefaultType: S.Finite.pipe(S.withDecodingDefaultType(Effect.succeed(0)))
117
196
  })
118
197
 
119
- /**
120
- * Like the default Schema `Literals` but with `withDefault` => literals[0]
121
- */
198
+ /** Like the default Schema `Literals` but with default helpers. Default value is `literals[0]`. */
122
199
  export const Literals = <const Literals extends NonEmptyReadonlyArray<AST.LiteralValue>>(literals: Literals) =>
123
200
  pipe(
124
201
  S.Literals(literals),
125
202
  (s) =>
126
203
  Object.assign(s, {
204
+ /** Override the default literal value used by `withConstructorDefault` / `withDecodingDefaultType`. */
127
205
  changeDefault: <A extends Literals[number]>(a: A) => {
128
206
  return Object.assign(S.Literals(literals), {
129
207
  Default: a,
130
- withDefault: s.pipe(S.withConstructorDefault(Effect.succeed(a))),
208
+ /**
209
+ * Construction-only default. Applied only when the field is
210
+ * omitted from `.make(...)` input. NOT applied during decode —
211
+ * cannot be used to JIT-migrate database fields. See file-level
212
+ * note.
213
+ */
214
+ withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.succeed(a))),
215
+ /**
216
+ * Decode-time default. **Discouraged for persisted data:** a
217
+ * missing field may be data corruption, not an old-shape
218
+ * document; silently substituting hides the problem. Prefer an
219
+ * explicit, preferably versioned migration over a decode-time
220
+ * fallback. See file-level note.
221
+ */
131
222
  withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.succeed(a)))
132
223
  }) // todo: copy annotations from original?
133
224
  },
134
225
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- load-bearing: Object.assign widens the field type without it, breaking `expectTypeOf(l.Default).toEqualTypeOf<"a">()` in tests
135
226
  Default: literals[0] as Literals[0],
136
- withDefault: s.pipe(S.withConstructorDefault(Effect.succeed(literals[0]))),
227
+ /**
228
+ * Construction-only default `literals[0]`. Applied only when the
229
+ * field is omitted from `.make(...)` input. NOT applied during
230
+ * decode — cannot be used to JIT-migrate database fields. See
231
+ * file-level note.
232
+ */
233
+ withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.succeed(literals[0]))),
234
+ /**
235
+ * Decode-time default `literals[0]`. **Discouraged for persisted
236
+ * data:** a missing field may be data corruption, not an old-shape
237
+ * document; silently substituting hides the problem. Prefer an
238
+ * explicit, preferably versioned migration over a decode-time
239
+ * fallback. See file-level note.
240
+ */
137
241
  withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.succeed(literals[0])))
138
242
  })
139
243
  )
140
244
 
141
- /**
142
- * Like the default Schema `Array` but with `withDefault` => []
143
- */
245
+ /** Like the default Schema `Array` but with default helpers. */
144
246
  export function Array<ValueSchema extends S.Top>(value: ValueSchema) {
145
247
  return pipe(
146
248
  S.Array(value).annotate(concurrencyUnbounded),
147
249
  (s) =>
148
250
  Object.assign(s, {
149
- withDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => []))),
251
+ /**
252
+ * Construction-only default `[]`. Applied only when the field is
253
+ * omitted from `.make(...)` input. NOT applied during decode —
254
+ * cannot be used to JIT-migrate database fields. See file-level
255
+ * note.
256
+ */
257
+ withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => []))),
258
+ /**
259
+ * Decode-time default `[]`. **Discouraged for persisted data:** a
260
+ * missing field may be data corruption, not an old-shape document;
261
+ * silently substituting `[]` hides the problem. Prefer an explicit,
262
+ * preferably versioned migration over a decode-time fallback. See
263
+ * file-level note.
264
+ */
150
265
  withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.sync(() => [])))
151
266
  })
152
267
  )
@@ -204,24 +319,35 @@ export const ReadonlyMapFromArray = <KeySchema extends S.Top, ValueSchema extend
204
319
  return schema
205
320
  }
206
321
 
207
- /**
208
- * Like the default Schema `ReadonlySet` but from Array with `withDefault` => new Set()
209
- */
322
+ /** Like the default Schema `ReadonlySet` but from Array, with default helpers. */
210
323
  export const ReadonlySet = <ValueSchema extends S.Top>(value: ValueSchema) =>
211
324
  pipe(
212
325
  ReadonlySetFromArray(value),
213
326
  (s) =>
214
327
  Object.assign(s, {
215
- withDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => new Set<S.Schema.Type<ValueSchema>>()))),
328
+ /**
329
+ * Construction-only default `new Set()`. Applied only when the field
330
+ * is omitted from `.make(...)` input. NOT applied during decode —
331
+ * cannot be used to JIT-migrate database fields. See file-level
332
+ * note.
333
+ */
334
+ withConstructorDefault: s.pipe(
335
+ S.withConstructorDefault(Effect.sync(() => new Set<S.Schema.Type<ValueSchema>>()))
336
+ ),
337
+ /**
338
+ * Decode-time default `new Set()`. **Discouraged for persisted
339
+ * data:** a missing field may be data corruption, not an old-shape
340
+ * document; silently substituting an empty set hides the problem.
341
+ * Prefer an explicit, preferably versioned migration over a
342
+ * decode-time fallback. See file-level note.
343
+ */
216
344
  withDecodingDefaultType: s.pipe(
217
345
  S.withDecodingDefaultType(Effect.sync(() => new Set<S.Schema.Type<ValueSchema>>()))
218
346
  )
219
347
  })
220
348
  )
221
349
 
222
- /**
223
- * Like the default Schema `ReadonlyMap` but from Array with `withDefault` => new Map()
224
- */
350
+ /** Like the default Schema `ReadonlyMap` but from Array, with default helpers. */
225
351
  export const ReadonlyMap = <KeySchema extends S.Top, ValueSchema extends S.Top>(pair: {
226
352
  readonly key: KeySchema
227
353
  readonly value: ValueSchema
@@ -230,39 +356,105 @@ export const ReadonlyMap = <KeySchema extends S.Top, ValueSchema extends S.Top>(
230
356
  ReadonlyMapFromArray(pair),
231
357
  (s) =>
232
358
  Object.assign(s, {
233
- withDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => new Map()))),
359
+ /**
360
+ * Construction-only default `new Map()`. Applied only when the field
361
+ * is omitted from `.make(...)` input. NOT applied during decode —
362
+ * cannot be used to JIT-migrate database fields. See file-level
363
+ * note.
364
+ */
365
+ withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => new Map()))),
366
+ /**
367
+ * Decode-time default `new Map()`. **Discouraged for persisted
368
+ * data:** a missing field may be data corruption, not an old-shape
369
+ * document; silently substituting an empty map hides the problem.
370
+ * Prefer an explicit, preferably versioned migration over a
371
+ * decode-time fallback. See file-level note.
372
+ */
234
373
  withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.sync(() => new Map())))
235
374
  })
236
375
  )
237
376
 
238
- /**
239
- * Like the default Schema `NullOr` but with `withDefault` => null
240
- */
377
+ /** Like the default Schema `NullOr` but with default helpers. */
241
378
  export const NullOr = <Schema extends S.Top>(self: Schema) =>
242
379
  pipe(
243
380
  S.NullOr(self),
244
381
  (s) =>
245
382
  Object.assign(s, {
246
- withDefault: s.pipe(S.withConstructorDefault(Effect.succeed(null))),
383
+ /**
384
+ * Construction-only default `null`. Applied only when the field is
385
+ * omitted from `.make(...)` input. NOT applied during decode —
386
+ * cannot be used to JIT-migrate database fields. See file-level
387
+ * note.
388
+ */
389
+ withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.succeed(null))),
390
+ /**
391
+ * Decode-time default `null`. **Discouraged for persisted data:** a
392
+ * missing field may be data corruption, not an old-shape document;
393
+ * silently substituting `null` hides the problem. Prefer an
394
+ * explicit, preferably versioned migration over a decode-time
395
+ * fallback. See file-level note.
396
+ */
247
397
  withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.succeed(null)))
248
398
  })
249
399
  )
250
400
 
401
+ /**
402
+ * Attach a `withConstructorDefault` of `new Date()` to any schema.
403
+ *
404
+ * **Construction-only.** Applied only when the field is omitted from
405
+ * `.make(...)` input. NOT applied during decode — cannot be used to
406
+ * JIT-migrate database fields. See file-level note.
407
+ */
251
408
  export const defaultDate = <Schema extends S.Top & S.WithoutConstructorDefault>(schema: Schema) =>
252
409
  schema.pipe(S.withConstructorDefault(Effect.sync(() => new global.Date())))
253
410
 
411
+ /**
412
+ * Attach a `withConstructorDefault` of `false` to any schema.
413
+ *
414
+ * **Construction-only.** Applied only when the field is omitted from
415
+ * `.make(...)` input. NOT applied during decode — cannot be used to
416
+ * JIT-migrate database fields. See file-level note.
417
+ */
254
418
  export const defaultBool = <Schema extends S.Top & S.WithoutConstructorDefault>(schema: Schema) =>
255
419
  schema.pipe(S.withConstructorDefault(Effect.succeed(false)))
256
420
 
421
+ /**
422
+ * Attach a `withConstructorDefault` of `null` to any schema.
423
+ *
424
+ * **Construction-only.** Applied only when the field is omitted from
425
+ * `.make(...)` input. NOT applied during decode — cannot be used to
426
+ * JIT-migrate database fields. See file-level note.
427
+ */
257
428
  export const defaultNullable = <Schema extends S.Top & S.WithoutConstructorDefault>(schema: Schema) =>
258
429
  schema.pipe(S.withConstructorDefault(Effect.succeed(null)))
259
430
 
431
+ /**
432
+ * Attach a `withConstructorDefault` of `[]` to any schema.
433
+ *
434
+ * **Construction-only.** Applied only when the field is omitted from
435
+ * `.make(...)` input. NOT applied during decode — cannot be used to
436
+ * JIT-migrate database fields. See file-level note.
437
+ */
260
438
  export const defaultArray = <Schema extends S.Top & S.WithoutConstructorDefault>(schema: Schema) =>
261
439
  schema.pipe(S.withConstructorDefault(Effect.sync(() => [])))
262
440
 
441
+ /**
442
+ * Attach a `withConstructorDefault` of `new Map()` to any schema.
443
+ *
444
+ * **Construction-only.** Applied only when the field is omitted from
445
+ * `.make(...)` input. NOT applied during decode — cannot be used to
446
+ * JIT-migrate database fields. See file-level note.
447
+ */
263
448
  export const defaultMap = <Schema extends S.Top & S.WithoutConstructorDefault>(schema: Schema) =>
264
449
  schema.pipe(S.withConstructorDefault(Effect.sync(() => new Map())))
265
450
 
451
+ /**
452
+ * Attach a `withConstructorDefault` of `new Set()` to any schema.
453
+ *
454
+ * **Construction-only.** Applied only when the field is omitted from
455
+ * `.make(...)` input. NOT applied during decode — cannot be used to
456
+ * JIT-migrate database fields. See file-level note.
457
+ */
266
458
  export const defaultSet = <Schema extends S.Top & S.WithoutConstructorDefault>(schema: Schema) =>
267
459
  schema.pipe(S.withConstructorDefault(Effect.sync(() => new Set())))
268
460
 
@@ -293,10 +485,23 @@ export type WithDefaults<Self extends S.Top> = (
293
485
  // export type UnionToIntersection3<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I
294
486
  // : never
295
487
 
488
+ /** Union of `DateValid` and `Date`, with default helpers. */
296
489
  export const inputDate = extendM(
297
490
  S.Union([S.DateValid, Date]),
298
491
  (s) => ({
299
- withDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => new globalThis.Date()))),
492
+ /**
493
+ * Construction-only default `new Date()`. Applied only when the field is
494
+ * omitted from `.make(...)` input. NOT applied during decode — cannot be
495
+ * used to JIT-migrate database fields. See file-level note.
496
+ */
497
+ withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => new globalThis.Date()))),
498
+ /**
499
+ * Decode-time default `new Date()`. **Discouraged for persisted data:** a
500
+ * missing field may be data corruption, not an old-shape document;
501
+ * silently substituting `new Date()` hides the problem. Prefer an
502
+ * explicit, preferably versioned migration over a decode-time fallback.
503
+ * See file-level note.
504
+ */
300
505
  withDecodingDefaultType: s.pipe(S.withDecodingDefaultType(Effect.sync(() => new globalThis.Date())))
301
506
  })
302
507
  )
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Branded string ID schemas with `.withConstructorDefault` extensions.
3
+ *
4
+ * Each `.withConstructorDefault` here is **only** applied when the field is
5
+ * omitted during construction (`.make(...)`). It is **not** applied during
6
+ * decode and therefore cannot be used to JIT-migrate database fields.
7
+ *
8
+ * For persisted data, prefer an explicit, preferably versioned migration
9
+ * over decode-time fallbacks. See `./ext.ts` for the full policy note.
10
+ */
1
11
  import { Effect, pipe } from "effect"
2
12
  import type { Refinement } from "effect-app/Function"
3
13
  import { extendM } from "effect-app/utils"
@@ -146,6 +156,9 @@ const StringIdArb = (): S.LazyArbitrary<StringId> => (fc) =>
146
156
  .map((_) => customRandom(urlAlphabet, size, (size) => _.subarray(0, size))() as StringId)
147
157
  /**
148
158
  * A string that is at least 6 characters long and a maximum of 50.
159
+ *
160
+ * `.withConstructorDefault` => fresh `nanoid()` (construction-only; not
161
+ * applied during decode — see file-level note).
149
162
  */
150
163
  export const StringId = extendM(
151
164
  pipe(
@@ -159,7 +172,13 @@ export const StringId = extendM(
159
172
  ),
160
173
  (s) => ({
161
174
  make: makeStringId,
162
- withDefault: s.pipe(S.withConstructorDefault(Effect.sync(makeStringId)))
175
+ /**
176
+ * Construction-only default: fresh `nanoid()`-shaped `StringId`. Applied
177
+ * only when the field is omitted from `.make(...)` input. NOT applied
178
+ * during decode — cannot be used to JIT-migrate database fields. See
179
+ * file-level note.
180
+ */
181
+ withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(makeStringId)))
163
182
  })
164
183
  )
165
184
  .pipe(withDefaultMake)
@@ -168,6 +187,14 @@ export const StringId = extendM(
168
187
 
169
188
  // const prefixedStringIdUnsafeThunk = (prefix: string) => () => prefixedStringIdUnsafe(prefix)
170
189
 
190
+ /**
191
+ * Build a `StringId` schema whose values are required to start with a fixed
192
+ * `prefix` (joined with `separator`, default `-`).
193
+ *
194
+ * The returned schema exposes `.withConstructorDefault` that mints a fresh
195
+ * prefixed id. Construction-only — not applied during decode; see file-level
196
+ * note.
197
+ */
171
198
  export function prefixedStringId<Type extends StringId>() {
172
199
  return <Prefix extends string, Separator extends string = "-">(
173
200
  prefix: Prefix,
@@ -206,20 +233,40 @@ export function prefixedStringId<Type extends StringId>() {
206
233
  */
207
234
  prefixSafe: <REST extends string>(str: `${Prefix}${Separator}${REST}`) => ex(str),
208
235
  prefix,
209
- withDefault: schema.pipe(S.withConstructorDefault<S.Codec<Type, string> & S.WithoutConstructorDefault>(
210
- Effect.sync(make)
211
- ))
236
+ /**
237
+ * Construction-only default: fresh prefixed id. Applied only when
238
+ * the field is omitted from `.make(...)` input. NOT applied during
239
+ * decode — cannot be used to JIT-migrate database fields. See
240
+ * file-level note.
241
+ */
242
+ withConstructorDefault: schema.pipe(
243
+ S.withConstructorDefault<S.Codec<Type, string> & S.WithoutConstructorDefault>(
244
+ Effect.sync(make)
245
+ )
246
+ )
212
247
  })
213
248
  )
214
249
  }
215
250
  }
216
251
 
252
+ /**
253
+ * Build a branded `StringId` schema for the given branded `Id` type.
254
+ *
255
+ * Exposes `.withConstructorDefault` that mints a fresh `nanoid()`-shaped id.
256
+ * Construction-only — not applied during decode; see file-level note.
257
+ */
217
258
  export const brandedStringId = <
218
259
  Id
219
260
  >() =>
220
261
  withDefaultMake(
221
262
  Object.assign(Object.create(StringId), StringId) as S.Codec<Id, string> & {
222
- withDefault: S.withConstructorDefault<S.Codec<Id, string> & S.WithoutConstructorDefault>
263
+ /**
264
+ * Construction-only default: fresh `nanoid()`-shaped id. Applied only
265
+ * when the field is omitted from `.make(...)` input. NOT applied
266
+ * during decode — cannot be used to JIT-migrate database fields. See
267
+ * file-level note.
268
+ */
269
+ withConstructorDefault: S.withConstructorDefault<S.Codec<Id, string> & S.WithoutConstructorDefault>
223
270
  make: () => Id
224
271
  } & WithDefaults<S.Codec<Id, string>>
225
272
  )
@@ -233,7 +280,12 @@ export interface PrefixedStringUtils<
233
280
  readonly unsafeFrom: (str: string) => Type
234
281
  prefixSafe: <REST extends string>(str: `${Prefix}${Separator}${REST}`) => Type
235
282
  readonly prefix: Prefix
236
- readonly withDefault: S.withConstructorDefault<S.Codec<Type, string> & S.WithoutConstructorDefault>
283
+ /**
284
+ * Construction-only default: fresh prefixed id. Applied only when the
285
+ * field is omitted from `.make(...)` input. NOT applied during decode —
286
+ * cannot be used to JIT-migrate database fields. See file-level note.
287
+ */
288
+ readonly withConstructorDefault: S.withConstructorDefault<S.Codec<Type, string> & S.WithoutConstructorDefault>
237
289
  }
238
290
 
239
291
  export interface UrlBrand extends Simplify<B.Brand<"Url"> & NonEmptyStringBrand> {}
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Numeric brand schemas with `.withConstructorDefault` extensions.
3
+ *
4
+ * Each `.withConstructorDefault` here is **only** applied when the field is
5
+ * omitted during construction (`.make(...)`). It is **not** applied during
6
+ * decode and therefore cannot be used to JIT-migrate database fields.
7
+ *
8
+ * For persisted data, prefer an explicit, preferably versioned migration
9
+ * over decode-time fallbacks. See `./ext.ts` for the full policy note.
10
+ */
1
11
  import { Effect } from "effect"
2
12
  import { extendM } from "effect-app/utils"
3
13
  import * as S from "effect/Schema"
@@ -9,17 +19,26 @@ import { type B } from "./schema.js"
9
19
  export interface PositiveIntBrand
10
20
  extends Simplify<B.Brand<"PositiveInt"> & NonNegativeIntBrand & PositiveNumberBrand>
11
21
  {}
22
+ /** Positive integer. `.withConstructorDefault` => `1` (construction-only). */
12
23
  export const PositiveInt = extendM(
13
24
  S.Int.pipe(
14
25
  S.check(S.isGreaterThan(0)),
15
26
  fromBrand<PositiveInt>(nominal<PositiveInt>(), { identifier: "PositiveInt", jsonSchema: {} }),
16
27
  withDefaultMake
17
28
  ),
18
- (s) => ({ withDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(1)))) })
29
+ (s) => ({
30
+ /**
31
+ * Construction-only default `1`. Applied only when the field is omitted
32
+ * from `.make(...)` input. NOT applied during decode — cannot be used to
33
+ * JIT-migrate database fields. See file-level note.
34
+ */
35
+ withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(1))))
36
+ })
19
37
  )
20
38
  export type PositiveInt = number & PositiveIntBrand
21
39
 
22
40
  export interface NonNegativeIntBrand extends Simplify<B.Brand<"NonNegativeInt"> & IntBrand & NonNegativeNumberBrand> {}
41
+ /** Non-negative integer. `.withConstructorDefault` => `0` (construction-only). */
23
42
  export const NonNegativeInt = extendM(
24
43
  S.Int.pipe(
25
44
  S.check(S.isGreaterThanOrEqualTo(0)),
@@ -29,18 +48,34 @@ export const NonNegativeInt = extendM(
29
48
  }),
30
49
  withDefaultMake
31
50
  ),
32
- (s) => ({ withDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(0)))) })
51
+ (s) => ({
52
+ /**
53
+ * Construction-only default `0`. Applied only when the field is omitted
54
+ * from `.make(...)` input. NOT applied during decode — cannot be used to
55
+ * JIT-migrate database fields. See file-level note.
56
+ */
57
+ withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(0))))
58
+ })
33
59
  )
34
60
  export type NonNegativeInt = number & NonNegativeIntBrand
35
61
 
36
62
  export interface IntBrand extends Simplify<B.Brand<"Int">> {}
63
+ /** Integer. `.withConstructorDefault` => `0` (construction-only). */
37
64
  export const Int = extendM(
38
65
  S.Int.pipe(fromBrand<Int>(nominal<Int>(), { identifier: "Int", jsonSchema: {} }), withDefaultMake),
39
- (s) => ({ withDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(0)))) })
66
+ (s) => ({
67
+ /**
68
+ * Construction-only default `0`. Applied only when the field is omitted
69
+ * from `.make(...)` input. NOT applied during decode — cannot be used to
70
+ * JIT-migrate database fields. See file-level note.
71
+ */
72
+ withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(0))))
73
+ })
40
74
  )
41
75
  export type Int = number & IntBrand
42
76
 
43
77
  export interface PositiveNumberBrand extends Simplify<B.Brand<"PositiveNumber"> & NonNegativeNumberBrand> {}
78
+ /** Positive finite number. `.withConstructorDefault` => `1` (construction-only). */
44
79
  export const PositiveNumber = extendM(
45
80
  S.Finite.pipe(
46
81
  S.check(S.isGreaterThan(0)),
@@ -50,11 +85,19 @@ export const PositiveNumber = extendM(
50
85
  }),
51
86
  withDefaultMake
52
87
  ),
53
- (s) => ({ withDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(1)))) })
88
+ (s) => ({
89
+ /**
90
+ * Construction-only default `1`. Applied only when the field is omitted
91
+ * from `.make(...)` input. NOT applied during decode — cannot be used to
92
+ * JIT-migrate database fields. See file-level note.
93
+ */
94
+ withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(1))))
95
+ })
54
96
  )
55
97
  export type PositiveNumber = number & PositiveNumberBrand
56
98
 
57
99
  export interface NonNegativeNumberBrand extends Simplify<B.Brand<"NonNegativeNumber">> {}
100
+ /** Non-negative finite number. `.withConstructorDefault` => `0` (construction-only). */
58
101
  export const NonNegativeNumber = extendM(
59
102
  S
60
103
  .Finite
@@ -66,7 +109,14 @@ export const NonNegativeNumber = extendM(
66
109
  }),
67
110
  withDefaultMake
68
111
  ),
69
- (s) => ({ withDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(0)))) })
112
+ (s) => ({
113
+ /**
114
+ * Construction-only default `0`. Applied only when the field is omitted
115
+ * from `.make(...)` input. NOT applied during decode — cannot be used to
116
+ * JIT-migrate database fields. See file-level note.
117
+ */
118
+ withConstructorDefault: s.pipe(S.withConstructorDefault(Effect.sync(() => s(0))))
119
+ })
70
120
  )
71
121
  export type NonNegativeNumber = number & NonNegativeNumberBrand
72
122