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/CHANGELOG.md +12 -0
- package/dist/Schema/ext.d.ts +284 -45
- package/dist/Schema/ext.d.ts.map +1 -1
- package/dist/Schema/ext.js +245 -42
- package/dist/Schema/moreStrings.d.ts +80 -10
- package/dist/Schema/moreStrings.d.ts.map +1 -1
- package/dist/Schema/moreStrings.js +42 -3
- package/dist/Schema/numbers.d.ts +48 -8
- package/dist/Schema/numbers.d.ts.map +1 -1
- package/dist/Schema/numbers.js +56 -6
- package/dist/Schema.d.ts +71 -1
- package/dist/Schema.d.ts.map +1 -1
- package/dist/Schema.js +91 -1
- package/dist/client/errors.d.ts +1 -1
- package/dist/ids.d.ts +33 -5
- package/dist/ids.d.ts.map +1 -1
- package/dist/ids.js +24 -3
- package/dist/rpc/Invalidation.d.ts +49 -49
- package/package.json +1 -1
- package/src/Schema/ext.ts +246 -41
- package/src/Schema/moreStrings.ts +58 -6
- package/src/Schema/numbers.ts +55 -5
- package/src/Schema.ts +96 -0
- package/src/ids.ts +23 -2
- package/test/schema.test.ts +8 -8
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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> {}
|
package/src/Schema/numbers.ts
CHANGED
|
@@ -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) => ({
|
|
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) => ({
|
|
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) => ({
|
|
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) => ({
|
|
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) => ({
|
|
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
|
|