honertia 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +610 -0
  3. package/dist/auth.d.ts +10 -0
  4. package/dist/auth.d.ts.map +1 -0
  5. package/dist/auth.js +11 -0
  6. package/dist/effect/action.d.ts +107 -0
  7. package/dist/effect/action.d.ts.map +1 -0
  8. package/dist/effect/action.js +150 -0
  9. package/dist/effect/auth.d.ts +94 -0
  10. package/dist/effect/auth.d.ts.map +1 -0
  11. package/dist/effect/auth.js +204 -0
  12. package/dist/effect/bridge.d.ts +40 -0
  13. package/dist/effect/bridge.d.ts.map +1 -0
  14. package/dist/effect/bridge.js +103 -0
  15. package/dist/effect/errors.d.ts +78 -0
  16. package/dist/effect/errors.d.ts.map +1 -0
  17. package/dist/effect/errors.js +37 -0
  18. package/dist/effect/handler.d.ts +25 -0
  19. package/dist/effect/handler.d.ts.map +1 -0
  20. package/dist/effect/handler.js +120 -0
  21. package/dist/effect/index.d.ts +16 -0
  22. package/dist/effect/index.d.ts.map +1 -0
  23. package/dist/effect/index.js +25 -0
  24. package/dist/effect/responses.d.ts +73 -0
  25. package/dist/effect/responses.d.ts.map +1 -0
  26. package/dist/effect/responses.js +104 -0
  27. package/dist/effect/routing.d.ts +90 -0
  28. package/dist/effect/routing.d.ts.map +1 -0
  29. package/dist/effect/routing.js +124 -0
  30. package/dist/effect/schema.d.ts +263 -0
  31. package/dist/effect/schema.d.ts.map +1 -0
  32. package/dist/effect/schema.js +586 -0
  33. package/dist/effect/services.d.ts +85 -0
  34. package/dist/effect/services.d.ts.map +1 -0
  35. package/dist/effect/services.js +24 -0
  36. package/dist/effect/validation.d.ts +38 -0
  37. package/dist/effect/validation.d.ts.map +1 -0
  38. package/dist/effect/validation.js +69 -0
  39. package/dist/helpers.d.ts +65 -0
  40. package/dist/helpers.d.ts.map +1 -0
  41. package/dist/helpers.js +116 -0
  42. package/dist/index.d.ts +14 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +26 -0
  45. package/dist/middleware.d.ts +14 -0
  46. package/dist/middleware.d.ts.map +1 -0
  47. package/dist/middleware.js +113 -0
  48. package/dist/react.d.ts +17 -0
  49. package/dist/react.d.ts.map +1 -0
  50. package/dist/react.js +4 -0
  51. package/dist/schema.d.ts +9 -0
  52. package/dist/schema.d.ts.map +1 -0
  53. package/dist/schema.js +34 -0
  54. package/dist/setup.d.ts +113 -0
  55. package/dist/setup.d.ts.map +1 -0
  56. package/dist/setup.js +96 -0
  57. package/dist/test-utils.d.ts +105 -0
  58. package/dist/test-utils.d.ts.map +1 -0
  59. package/dist/test-utils.js +210 -0
  60. package/dist/types.d.ts +37 -0
  61. package/dist/types.d.ts.map +1 -0
  62. package/dist/types.js +11 -0
  63. package/package.json +71 -0
@@ -0,0 +1,586 @@
1
+ /**
2
+ * Honertia Effect Schema Validators
3
+ *
4
+ * Laravel-inspired Effect Schema helpers for common validation patterns.
5
+ */
6
+ import { Schema as S } from 'effect';
7
+ // =============================================================================
8
+ // String Types
9
+ // =============================================================================
10
+ /**
11
+ * Trims whitespace from a string.
12
+ */
13
+ export const trimmed = S.transform(S.String, S.String, {
14
+ decode: (s) => s.trim(),
15
+ encode: (s) => s,
16
+ });
17
+ /**
18
+ * A nullable string that converts empty/whitespace-only strings to null.
19
+ * Useful for optional text fields where empty input should be stored as null.
20
+ */
21
+ export const nullableString = S.transform(S.Unknown, S.NullOr(S.String), {
22
+ decode: (value) => {
23
+ if (value === undefined || value === null)
24
+ return null;
25
+ if (typeof value === 'string') {
26
+ const trimmed = value.trim();
27
+ return trimmed === '' ? null : trimmed;
28
+ }
29
+ return String(value);
30
+ },
31
+ encode: (s) => s,
32
+ });
33
+ /**
34
+ * Alias for nullableString.
35
+ */
36
+ export const optionalString = nullableString;
37
+ /**
38
+ * A required string that is trimmed. Empty strings fail validation.
39
+ */
40
+ export const requiredString = S.String.pipe(S.transform(S.String, {
41
+ decode: (s) => s.trim(),
42
+ encode: (s) => s,
43
+ }), S.minLength(1, { message: () => 'This field is required' }));
44
+ /**
45
+ * Create a required string with a custom message.
46
+ */
47
+ export const required = (message = 'This field is required') => S.String.pipe(S.transform(S.String, {
48
+ decode: (s) => s.trim(),
49
+ encode: (s) => s,
50
+ }), S.minLength(1, { message: () => message }));
51
+ // =============================================================================
52
+ // Numeric Types
53
+ // =============================================================================
54
+ /**
55
+ * Coerces a value to a number.
56
+ */
57
+ export const coercedNumber = S.transform(S.Unknown, S.Number, {
58
+ decode: (value) => {
59
+ if (typeof value === 'number')
60
+ return value;
61
+ if (typeof value === 'string') {
62
+ const parsed = parseFloat(value);
63
+ if (!isNaN(parsed))
64
+ return parsed;
65
+ }
66
+ throw new Error('Expected a number');
67
+ },
68
+ encode: (n) => n,
69
+ });
70
+ /**
71
+ * Coerces a value to a positive integer.
72
+ */
73
+ export const positiveInt = coercedNumber.pipe(S.int({ message: () => 'Must be an integer' }), S.positive({ message: () => 'Must be positive' }));
74
+ /**
75
+ * Coerces a value to a non-negative integer (0 or greater).
76
+ */
77
+ export const nonNegativeInt = coercedNumber.pipe(S.int({ message: () => 'Must be an integer' }), S.nonNegative({ message: () => 'Must be non-negative' }));
78
+ /**
79
+ * Parses a string to a positive integer, returning null on failure.
80
+ */
81
+ export function parsePositiveInt(value) {
82
+ if (value === undefined)
83
+ return null;
84
+ const parsed = parseInt(value, 10);
85
+ if (isNaN(parsed) || parsed <= 0)
86
+ return null;
87
+ return parsed;
88
+ }
89
+ // =============================================================================
90
+ // Boolean Types
91
+ // =============================================================================
92
+ /**
93
+ * Coerces various truthy/falsy values to boolean.
94
+ */
95
+ export const coercedBoolean = S.transform(S.Unknown, S.Boolean, {
96
+ decode: (value) => {
97
+ if (typeof value === 'boolean')
98
+ return value;
99
+ if (typeof value === 'number')
100
+ return value !== 0;
101
+ if (typeof value === 'string') {
102
+ const lower = value.toLowerCase().trim();
103
+ if (['true', '1', 'on', 'yes'].includes(lower))
104
+ return true;
105
+ if (['false', '0', 'off', 'no', ''].includes(lower))
106
+ return false;
107
+ }
108
+ return Boolean(value);
109
+ },
110
+ encode: (b) => b,
111
+ });
112
+ /**
113
+ * A checkbox value that defaults to false if not present.
114
+ */
115
+ export const checkbox = S.transform(S.Unknown, S.Boolean, {
116
+ decode: (value) => {
117
+ if (value === undefined || value === null || value === '')
118
+ return false;
119
+ if (typeof value === 'boolean')
120
+ return value;
121
+ if (typeof value === 'string') {
122
+ const lower = value.toLowerCase().trim();
123
+ return ['true', '1', 'on', 'yes'].includes(lower);
124
+ }
125
+ return Boolean(value);
126
+ },
127
+ encode: (b) => b,
128
+ });
129
+ // =============================================================================
130
+ // Date Types
131
+ // =============================================================================
132
+ /**
133
+ * Coerces a string or number to a Date object.
134
+ */
135
+ export const coercedDate = S.transform(S.Unknown, S.DateFromSelf, {
136
+ decode: (value) => {
137
+ if (value instanceof Date)
138
+ return value;
139
+ if (typeof value === 'string' || typeof value === 'number') {
140
+ const date = new Date(value);
141
+ if (!isNaN(date.getTime()))
142
+ return date;
143
+ }
144
+ throw new Error('Expected a valid date');
145
+ },
146
+ encode: (d) => d,
147
+ });
148
+ /**
149
+ * A nullable date that accepts empty strings as null.
150
+ */
151
+ export const nullableDate = S.transform(S.Unknown, S.NullOr(S.DateFromSelf), {
152
+ decode: (value) => {
153
+ if (value === undefined || value === null || value === '')
154
+ return null;
155
+ if (value instanceof Date)
156
+ return value;
157
+ if (typeof value === 'string' || typeof value === 'number') {
158
+ const date = new Date(value);
159
+ if (!isNaN(date.getTime()))
160
+ return date;
161
+ }
162
+ throw new Error('Expected a valid date');
163
+ },
164
+ encode: (d) => d,
165
+ });
166
+ // =============================================================================
167
+ // Array Types
168
+ // =============================================================================
169
+ /**
170
+ * Ensures a value is always an array.
171
+ */
172
+ export const ensureArray = (schema) => S.transform(S.Unknown, S.Array(schema), {
173
+ decode: (value) => {
174
+ if (value === undefined || value === null)
175
+ return [];
176
+ if (Array.isArray(value))
177
+ return value;
178
+ return [value];
179
+ },
180
+ encode: (arr) => arr,
181
+ });
182
+ // =============================================================================
183
+ // Common Patterns
184
+ // =============================================================================
185
+ /**
186
+ * An email address with trimming and lowercase normalization.
187
+ */
188
+ export const email = S.String.pipe(S.transform(S.String, {
189
+ decode: (s) => s.trim().toLowerCase(),
190
+ encode: (s) => s,
191
+ }), S.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, { message: () => 'Invalid email address' }));
192
+ /**
193
+ * A nullable email address.
194
+ */
195
+ export const nullableEmail = S.transform(S.Unknown, S.NullOr(S.String), {
196
+ decode: (value) => {
197
+ if (value === undefined || value === null)
198
+ return null;
199
+ if (typeof value === 'string') {
200
+ const trimmed = value.trim().toLowerCase();
201
+ if (trimmed === '')
202
+ return null;
203
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
204
+ throw new Error('Invalid email address');
205
+ }
206
+ return trimmed;
207
+ }
208
+ throw new Error('Expected a string');
209
+ },
210
+ encode: (s) => s,
211
+ });
212
+ /**
213
+ * A URL with trimming.
214
+ */
215
+ export const url = S.String.pipe(S.transform(S.String, {
216
+ decode: (s) => s.trim(),
217
+ encode: (s) => s,
218
+ }), S.filter((s) => {
219
+ try {
220
+ new URL(s);
221
+ return true;
222
+ }
223
+ catch {
224
+ return false;
225
+ }
226
+ }, { message: () => 'Invalid URL' }));
227
+ /**
228
+ * A nullable URL.
229
+ */
230
+ export const nullableUrl = S.transform(S.Unknown, S.NullOr(S.String), {
231
+ decode: (value) => {
232
+ if (value === undefined || value === null)
233
+ return null;
234
+ if (typeof value === 'string') {
235
+ const trimmed = value.trim();
236
+ if (trimmed === '')
237
+ return null;
238
+ try {
239
+ new URL(trimmed);
240
+ return trimmed;
241
+ }
242
+ catch {
243
+ throw new Error('Invalid URL');
244
+ }
245
+ }
246
+ throw new Error('Expected a string');
247
+ },
248
+ encode: (s) => s,
249
+ });
250
+ // =============================================================================
251
+ // Laravel-style String Rules
252
+ // =============================================================================
253
+ /**
254
+ * Validates that a string contains only alphabetic characters.
255
+ */
256
+ export const alpha = S.String.pipe(S.transform(S.String, {
257
+ decode: (s) => s.trim(),
258
+ encode: (s) => s,
259
+ }), S.pattern(/^[a-zA-Z]+$/, { message: () => 'Must contain only letters' }));
260
+ /**
261
+ * Validates that a string contains only alphanumeric characters, dashes, and underscores.
262
+ */
263
+ export const alphaDash = S.String.pipe(S.transform(S.String, {
264
+ decode: (s) => s.trim(),
265
+ encode: (s) => s,
266
+ }), S.pattern(/^[a-zA-Z0-9_-]+$/, {
267
+ message: () => 'Must contain only letters, numbers, dashes, and underscores',
268
+ }));
269
+ /**
270
+ * Validates that a string contains only alphanumeric characters.
271
+ */
272
+ export const alphaNum = S.String.pipe(S.transform(S.String, {
273
+ decode: (s) => s.trim(),
274
+ encode: (s) => s,
275
+ }), S.pattern(/^[a-zA-Z0-9]+$/, { message: () => 'Must contain only letters and numbers' }));
276
+ /**
277
+ * Validates a string starts with one of the given values.
278
+ */
279
+ export const startsWith = (prefixes, message) => S.String.pipe(S.filter((val) => prefixes.some((prefix) => val.startsWith(prefix)), { message: () => message ?? `Must start with one of: ${prefixes.join(', ')}` }));
280
+ /**
281
+ * Validates a string ends with one of the given values.
282
+ */
283
+ export const endsWith = (suffixes, message) => S.String.pipe(S.filter((val) => suffixes.some((suffix) => val.endsWith(suffix)), { message: () => message ?? `Must end with one of: ${suffixes.join(', ')}` }));
284
+ /**
285
+ * Validates that a string is all lowercase.
286
+ */
287
+ export const lowercase = S.String.pipe(S.filter((val) => val === val.toLowerCase(), { message: () => 'Must be lowercase' }));
288
+ /**
289
+ * Validates that a string is all uppercase.
290
+ */
291
+ export const uppercase = S.String.pipe(S.filter((val) => val === val.toUpperCase(), { message: () => 'Must be uppercase' }));
292
+ // =============================================================================
293
+ // Laravel-style Numeric Rules
294
+ // =============================================================================
295
+ /**
296
+ * Validates a number is between min and max (inclusive).
297
+ */
298
+ export const between = (min, max, message) => coercedNumber.pipe(S.between(min, max, { message: () => message ?? `Must be between ${min} and ${max}` }));
299
+ /**
300
+ * Validates that a value has exactly the specified number of digits.
301
+ */
302
+ export const digits = (length, message) => S.String.pipe(S.pattern(new RegExp(`^\\d{${length}}$`), { message: () => message ?? `Must be exactly ${length} digits` }));
303
+ /**
304
+ * Validates that a value has between min and max digits.
305
+ */
306
+ export const digitsBetween = (min, max, message) => S.String.pipe(S.pattern(new RegExp(`^\\d{${min},${max}}$`), { message: () => message ?? `Must be between ${min} and ${max} digits` }));
307
+ /**
308
+ * Validates a number is greater than the given value.
309
+ */
310
+ export const gt = (value, message) => coercedNumber.pipe(S.greaterThan(value, { message: () => message ?? `Must be greater than ${value}` }));
311
+ /**
312
+ * Validates a number is greater than or equal to the given value.
313
+ */
314
+ export const gte = (value, message) => coercedNumber.pipe(S.greaterThanOrEqualTo(value, { message: () => message ?? `Must be at least ${value}` }));
315
+ /**
316
+ * Validates a number is less than the given value.
317
+ */
318
+ export const lt = (value, message) => coercedNumber.pipe(S.lessThan(value, { message: () => message ?? `Must be less than ${value}` }));
319
+ /**
320
+ * Validates a number is less than or equal to the given value.
321
+ */
322
+ export const lte = (value, message) => coercedNumber.pipe(S.lessThanOrEqualTo(value, { message: () => message ?? `Must be at most ${value}` }));
323
+ /**
324
+ * Validates a number is a multiple of another number.
325
+ */
326
+ export const multipleOf = (value, message) => coercedNumber.pipe(S.multipleOf(value, { message: () => message ?? `Must be a multiple of ${value}` }));
327
+ // =============================================================================
328
+ // Laravel-style Enum/In Rules
329
+ // =============================================================================
330
+ /**
331
+ * Validates that a value is one of the allowed values.
332
+ */
333
+ export const inArray = (values, message) => S.String.pipe(S.filter((val) => values.includes(val), { message: () => message ?? `Must be one of: ${values.join(', ')}` }));
334
+ /**
335
+ * Validates that a value is NOT one of the disallowed values.
336
+ */
337
+ export const notIn = (values, message) => S.Unknown.pipe(S.filter((val) => !values.includes(val), { message: () => message ?? `Must not be one of: ${values.join(', ')}` }));
338
+ // =============================================================================
339
+ // Laravel-style Format Rules
340
+ // =============================================================================
341
+ /**
342
+ * Validates a UUID.
343
+ */
344
+ export const uuid = S.UUID;
345
+ /**
346
+ * Validates a nullable UUID.
347
+ */
348
+ export const nullableUuid = S.transform(S.Unknown, S.NullOr(S.UUID), {
349
+ decode: (value) => {
350
+ if (value === undefined || value === null || value === '')
351
+ return null;
352
+ if (typeof value !== 'string')
353
+ throw new Error('Expected a string');
354
+ return value;
355
+ },
356
+ encode: (s) => s,
357
+ });
358
+ /**
359
+ * Validates an IPv4 address.
360
+ */
361
+ export const ipv4 = S.String.pipe(S.pattern(/^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/, { message: () => 'Must be a valid IPv4 address' }));
362
+ /**
363
+ * Validates an IPv6 address.
364
+ */
365
+ export const ipv6 = S.String.pipe(S.pattern(/^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$|^::(?:[a-fA-F0-9]{1,4}:){0,5}[a-fA-F0-9]{1,4}$|^[a-fA-F0-9]{1,4}::(?:[a-fA-F0-9]{1,4}:){0,4}[a-fA-F0-9]{1,4}$|^(?:[a-fA-F0-9]{1,4}:){2}:(?:[a-fA-F0-9]{1,4}:){0,3}[a-fA-F0-9]{1,4}$|^(?:[a-fA-F0-9]{1,4}:){3}:(?:[a-fA-F0-9]{1,4}:){0,2}[a-fA-F0-9]{1,4}$|^(?:[a-fA-F0-9]{1,4}:){4}:(?:[a-fA-F0-9]{1,4}:)?[a-fA-F0-9]{1,4}$|^(?:[a-fA-F0-9]{1,4}:){5}:[a-fA-F0-9]{1,4}$|^(?:[a-fA-F0-9]{1,4}:){6}:$/, { message: () => 'Must be a valid IPv6 address' }));
366
+ /**
367
+ * Validates an IP address (v4 or v6).
368
+ */
369
+ export const ip = S.String.pipe(S.filter((val) => /^(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/.test(val) ||
370
+ /^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$/.test(val), { message: () => 'Must be a valid IP address' }));
371
+ /**
372
+ * Validates a MAC address.
373
+ */
374
+ export const macAddress = S.String.pipe(S.pattern(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/, { message: () => 'Must be a valid MAC address' }));
375
+ /**
376
+ * Validates valid JSON string.
377
+ */
378
+ export const jsonString = S.String.pipe(S.filter((val) => {
379
+ try {
380
+ JSON.parse(val);
381
+ return true;
382
+ }
383
+ catch {
384
+ return false;
385
+ }
386
+ }, { message: () => 'Must be valid JSON' }));
387
+ // =============================================================================
388
+ // Laravel-style Confirmation Rules
389
+ // =============================================================================
390
+ /**
391
+ * Helper for password confirmation validation.
392
+ * Use with S.Struct and filter for cross-field validation.
393
+ */
394
+ export function confirmed(fieldName, confirmationFieldName = `${fieldName}_confirmation`, message = 'Confirmation does not match') {
395
+ return {
396
+ fieldName,
397
+ confirmationFieldName,
398
+ refine: (data) => data[fieldName] === data[confirmationFieldName],
399
+ message,
400
+ path: [confirmationFieldName],
401
+ };
402
+ }
403
+ // =============================================================================
404
+ // Laravel-style Accepted Rules
405
+ // =============================================================================
406
+ /**
407
+ * Validates that a value is "accepted" (true, "yes", "on", "1", 1).
408
+ */
409
+ export const accepted = S.transform(S.Unknown, S.Literal(true), {
410
+ strict: false,
411
+ decode: (value) => {
412
+ if (typeof value === 'boolean')
413
+ return value;
414
+ if (typeof value === 'number')
415
+ return value === 1;
416
+ if (typeof value === 'string') {
417
+ const lower = value.toLowerCase().trim();
418
+ return ['true', '1', 'on', 'yes'].includes(lower);
419
+ }
420
+ return false;
421
+ },
422
+ encode: () => true,
423
+ }).pipe(S.filter((v) => v === true, { message: () => 'Must be accepted' }));
424
+ /**
425
+ * Validates that a value is "declined" (false, "no", "off", "0", 0).
426
+ */
427
+ export const declined = S.transform(S.Unknown, S.Literal(false), {
428
+ strict: false,
429
+ decode: (value) => {
430
+ if (typeof value === 'boolean')
431
+ return value;
432
+ if (typeof value === 'number')
433
+ return value === 0;
434
+ if (typeof value === 'string') {
435
+ const lower = value.toLowerCase().trim();
436
+ return ['false', '0', 'off', 'no'].includes(lower);
437
+ }
438
+ return true;
439
+ },
440
+ encode: () => false,
441
+ }).pipe(S.filter((v) => v === false, { message: () => 'Must be declined' }));
442
+ // =============================================================================
443
+ // Laravel-style Size Rules
444
+ // =============================================================================
445
+ /**
446
+ * Validates exact string length.
447
+ */
448
+ export const size = (length, message) => S.String.pipe(S.length(length, { message: () => message ?? `Must be exactly ${length} characters` }));
449
+ /**
450
+ * Validates minimum string length.
451
+ */
452
+ export const min = (length, message) => S.String.pipe(S.minLength(length, { message: () => message ?? `Must be at least ${length} characters` }));
453
+ /**
454
+ * Validates maximum string length.
455
+ */
456
+ export const max = (length, message) => S.String.pipe(S.maxLength(length, { message: () => message ?? `Must be at most ${length} characters` }));
457
+ // =============================================================================
458
+ // Laravel-style Date Rules
459
+ // =============================================================================
460
+ /**
461
+ * Validates a date is after the given date.
462
+ */
463
+ export const after = (date, message) => {
464
+ const compareDate = typeof date === 'string' ? new Date(date) : date;
465
+ return coercedDate.pipe(S.filter((val) => val > compareDate, { message: () => message ?? `Must be after ${compareDate.toISOString()}` }));
466
+ };
467
+ /**
468
+ * Validates a date is after or equal to the given date.
469
+ */
470
+ export const afterOrEqual = (date, message) => {
471
+ const compareDate = typeof date === 'string' ? new Date(date) : date;
472
+ return coercedDate.pipe(S.filter((val) => val >= compareDate, { message: () => message ?? `Must be on or after ${compareDate.toISOString()}` }));
473
+ };
474
+ /**
475
+ * Validates a date is before the given date.
476
+ */
477
+ export const before = (date, message) => {
478
+ const compareDate = typeof date === 'string' ? new Date(date) : date;
479
+ return coercedDate.pipe(S.filter((val) => val < compareDate, { message: () => message ?? `Must be before ${compareDate.toISOString()}` }));
480
+ };
481
+ /**
482
+ * Validates a date is before or equal to the given date.
483
+ */
484
+ export const beforeOrEqual = (date, message) => {
485
+ const compareDate = typeof date === 'string' ? new Date(date) : date;
486
+ return coercedDate.pipe(S.filter((val) => val <= compareDate, { message: () => message ?? `Must be on or before ${compareDate.toISOString()}` }));
487
+ };
488
+ // =============================================================================
489
+ // Laravel-style Array Rules
490
+ // =============================================================================
491
+ /**
492
+ * Validates array has distinct/unique values.
493
+ */
494
+ export const distinct = (schema, message) => S.Array(schema).pipe(S.filter((arr) => new Set(arr).size === arr.length, { message: () => message ?? 'Must contain unique values' }));
495
+ /**
496
+ * Validates array has minimum number of items.
497
+ */
498
+ export const minItems = (schema, minCount, message) => S.Array(schema).pipe(S.minItems(minCount, { message: () => message ?? `Must have at least ${minCount} items` }));
499
+ /**
500
+ * Validates array has maximum number of items.
501
+ */
502
+ export const maxItems = (schema, maxCount, message) => S.Array(schema).pipe(S.maxItems(maxCount, { message: () => message ?? `Must have at most ${maxCount} items` }));
503
+ // =============================================================================
504
+ // Laravel-style Password Rules
505
+ // =============================================================================
506
+ /**
507
+ * Creates a password schema with configurable rules.
508
+ */
509
+ export function password(options = {}) {
510
+ const { min: minLength = 8, max: maxLength, letters = false, mixedCase = false, numbers = false, symbols = false, } = options;
511
+ let schema = S.String.pipe(S.minLength(minLength, { message: () => `Password must be at least ${minLength} characters` }));
512
+ if (maxLength) {
513
+ schema = schema.pipe(S.maxLength(maxLength, { message: () => `Password must be at most ${maxLength} characters` }));
514
+ }
515
+ if (letters) {
516
+ schema = schema.pipe(S.filter((val) => /[a-zA-Z]/.test(val), {
517
+ message: () => 'Password must contain at least one letter',
518
+ }));
519
+ }
520
+ if (mixedCase) {
521
+ schema = schema.pipe(S.filter((val) => /[a-z]/.test(val) && /[A-Z]/.test(val), {
522
+ message: () => 'Password must contain both uppercase and lowercase letters',
523
+ }));
524
+ }
525
+ if (numbers) {
526
+ schema = schema.pipe(S.filter((val) => /\d/.test(val), {
527
+ message: () => 'Password must contain at least one number',
528
+ }));
529
+ }
530
+ if (symbols) {
531
+ schema = schema.pipe(S.filter((val) => /[!@#$%^&*(),.?":{}|<>]/.test(val), {
532
+ message: () => 'Password must contain at least one special character',
533
+ }));
534
+ }
535
+ return schema;
536
+ }
537
+ // =============================================================================
538
+ // Laravel-style Conditional Rules
539
+ // =============================================================================
540
+ /**
541
+ * Excludes a field (sets to undefined) when a condition is met.
542
+ */
543
+ export const excludeIf = (schema, condition) => S.transform(S.Unknown, S.Union(schema, S.Undefined), {
544
+ decode: (value) => {
545
+ if (condition(value))
546
+ return undefined;
547
+ return value;
548
+ },
549
+ encode: (value) => value,
550
+ });
551
+ // =============================================================================
552
+ // Utility Functions
553
+ // =============================================================================
554
+ /**
555
+ * Creates a nullable version of any schema.
556
+ * Converts empty strings to null.
557
+ */
558
+ export const nullable = (schema) => S.transform(S.Unknown, S.NullOr(schema), {
559
+ decode: (value) => {
560
+ if (value === undefined || value === null)
561
+ return null;
562
+ if (typeof value === 'string' && value.trim() === '')
563
+ return null;
564
+ return value;
565
+ },
566
+ encode: (value) => value,
567
+ });
568
+ /**
569
+ * Creates a schema that fills in a default value when empty/null/undefined.
570
+ */
571
+ export const filled = (schema, defaultValue) => S.transform(S.Unknown, schema, {
572
+ strict: false,
573
+ decode: (value) => {
574
+ if (value === undefined || value === null || value === '')
575
+ return defaultValue;
576
+ return value;
577
+ },
578
+ encode: (value) => value,
579
+ });
580
+ // =============================================================================
581
+ // Schema Helpers
582
+ // =============================================================================
583
+ /**
584
+ * Re-export Schema namespace for convenience.
585
+ */
586
+ export { Schema as S } from 'effect';
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Effect Services for Honertia
3
+ *
4
+ * Service tags for dependency injection via Effect.
5
+ */
6
+ import { Context } from 'effect';
7
+ declare const DatabaseService_base: Context.TagClass<DatabaseService, "honertia/Database", unknown>;
8
+ /**
9
+ * Database Service - Generic database client
10
+ */
11
+ export declare class DatabaseService extends DatabaseService_base {
12
+ }
13
+ declare const AuthService_base: Context.TagClass<AuthService, "honertia/Auth", unknown>;
14
+ /**
15
+ * Auth Service - Better-auth instance
16
+ */
17
+ export declare class AuthService extends AuthService_base {
18
+ }
19
+ /**
20
+ * Authenticated User - Session with user data
21
+ */
22
+ export interface AuthUser {
23
+ user: {
24
+ id: string;
25
+ email: string;
26
+ name: string | null;
27
+ emailVerified: boolean;
28
+ image: string | null;
29
+ createdAt: Date;
30
+ updatedAt: Date;
31
+ };
32
+ session: {
33
+ id: string;
34
+ userId: string;
35
+ expiresAt: Date;
36
+ token: string;
37
+ createdAt: Date;
38
+ updatedAt: Date;
39
+ };
40
+ }
41
+ declare const AuthUserService_base: Context.TagClass<AuthUserService, "honertia/AuthUser", AuthUser>;
42
+ export declare class AuthUserService extends AuthUserService_base {
43
+ }
44
+ /**
45
+ * Honertia Renderer - Inertia-style page rendering
46
+ */
47
+ export interface HonertiaRenderer {
48
+ render<T extends Record<string, unknown>>(component: string, props?: T): Promise<Response>;
49
+ share(key: string, value: unknown): void;
50
+ setErrors(errors: Record<string, string>): void;
51
+ }
52
+ declare const HonertiaService_base: Context.TagClass<HonertiaService, "honertia/Honertia", HonertiaRenderer>;
53
+ export declare class HonertiaService extends HonertiaService_base {
54
+ }
55
+ /**
56
+ * Request Context - HTTP request data
57
+ */
58
+ export interface RequestContext {
59
+ readonly method: string;
60
+ readonly url: string;
61
+ readonly headers: Headers;
62
+ param(name: string): string | undefined;
63
+ params(): Record<string, string>;
64
+ query(): Record<string, string>;
65
+ json<T = unknown>(): Promise<T>;
66
+ parseBody(): Promise<Record<string, unknown>>;
67
+ header(name: string): string | undefined;
68
+ }
69
+ declare const RequestService_base: Context.TagClass<RequestService, "honertia/Request", RequestContext>;
70
+ export declare class RequestService extends RequestService_base {
71
+ }
72
+ /**
73
+ * Response Factory - Create HTTP responses
74
+ */
75
+ export interface ResponseFactory {
76
+ redirect(url: string, status?: number): Response;
77
+ json<T>(data: T, status?: number): Response;
78
+ text(data: string, status?: number): Response;
79
+ notFound(): Response | Promise<Response>;
80
+ }
81
+ declare const ResponseFactoryService_base: Context.TagClass<ResponseFactoryService, "honertia/ResponseFactory", ResponseFactory>;
82
+ export declare class ResponseFactoryService extends ResponseFactoryService_base {
83
+ }
84
+ export {};
85
+ //# sourceMappingURL=services.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../src/effect/services.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAU,MAAM,QAAQ,CAAA;;AAExC;;GAEG;AACH,qBAAa,eAAgB,SAAQ,oBAGlC;CAAG;;AAEN;;GAEG;AACH,qBAAa,WAAY,SAAQ,gBAG9B;CAAG;AAEN;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE;QACJ,EAAE,EAAE,MAAM,CAAA;QACV,KAAK,EAAE,MAAM,CAAA;QACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;QACnB,aAAa,EAAE,OAAO,CAAA;QACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;QACpB,SAAS,EAAE,IAAI,CAAA;QACf,SAAS,EAAE,IAAI,CAAA;KAChB,CAAA;IACD,OAAO,EAAE;QACP,EAAE,EAAE,MAAM,CAAA;QACV,MAAM,EAAE,MAAM,CAAA;QACd,SAAS,EAAE,IAAI,CAAA;QACf,KAAK,EAAE,MAAM,CAAA;QACb,SAAS,EAAE,IAAI,CAAA;QACf,SAAS,EAAE,IAAI,CAAA;KAChB,CAAA;CACF;;AAED,qBAAa,eAAgB,SAAQ,oBAGlC;CAAG;AAEN;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,SAAS,EAAE,MAAM,EACjB,KAAK,CAAC,EAAE,CAAC,GACR,OAAO,CAAC,QAAQ,CAAC,CAAA;IACpB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAA;IACxC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;CAChD;;AAED,qBAAa,eAAgB,SAAQ,oBAGlC;CAAG;AAEN;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IACvC,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,IAAI,CAAC,CAAC,GAAG,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;IAC/B,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IAC7C,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CACzC;;AAED,qBAAa,cAAe,SAAQ,mBAGjC;CAAG;AAEN;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAA;IAChD,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAA;IAC3C,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAA;IAC7C,QAAQ,IAAI,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CACzC;;AAED,qBAAa,sBAAuB,SAAQ,2BAGzC;CAAG"}