@active-record-ts/active-model 1.0.0 → 1.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.
@@ -0,0 +1,766 @@
1
+ /**
2
+ * Type system for ActiveModel attributes.
3
+ *
4
+ * Each Type owns three coercion functions:
5
+ * - `cast` converts a value coming from user code (`record.name = 1`)
6
+ * into the canonical attribute value (e.g. `"1"` for a string column).
7
+ * - `serialize` converts an attribute value into a driver-compatible
8
+ * value for INSERT/UPDATE statements.
9
+ * - `deserialize` converts a value coming back from the driver into an
10
+ * attribute value.
11
+ *
12
+ * The named type registry mirrors Rails' `ActiveModel::Type` registry.
13
+ * Adapters can register adapter-specific types (e.g. PostgreSQL's `uuid`
14
+ * or `jsonb`) by calling `Type.register(name, factory)`.
15
+ */
16
+ /** Base interface every type implementation satisfies. */
17
+ interface Type<T = unknown> {
18
+ /** Name of the type as registered (e.g. `'string'`, `'integer'`). */
19
+ readonly type: string;
20
+ cast(value: unknown): T | null;
21
+ serialize(value: T | null): unknown;
22
+ deserialize(value: unknown): T | null;
23
+ /** Returns true when the two values represent the same logical attribute value. */
24
+ equals?(a: T | null, b: T | null): boolean;
25
+ }
26
+ declare class StringType implements Type<string> {
27
+ readonly type = "string";
28
+ cast(value: unknown): string | null;
29
+ serialize(value: string | null): string | null;
30
+ deserialize(value: unknown): string | null;
31
+ }
32
+ declare class IntegerType implements Type<number> {
33
+ readonly type = "integer";
34
+ cast(value: unknown): number | null;
35
+ serialize(value: number | null): number | null;
36
+ deserialize(value: unknown): number | null;
37
+ }
38
+ declare class BigIntType implements Type<bigint> {
39
+ readonly type = "bigint";
40
+ cast(value: unknown): bigint | null;
41
+ serialize(value: bigint | null): bigint | null;
42
+ deserialize(value: unknown): bigint | null;
43
+ }
44
+ declare class FloatType implements Type<number> {
45
+ readonly type = "float";
46
+ cast(value: unknown): number | null;
47
+ serialize(value: number | null): number | null;
48
+ deserialize(value: unknown): number | null;
49
+ }
50
+ /**
51
+ * Stored as a string to preserve precision. Drivers commonly return decimals
52
+ * as strings; we keep them that way for callers to feed into a decimal lib
53
+ * of choice (we don't ship one).
54
+ */
55
+ declare class DecimalType implements Type<string> {
56
+ readonly type = "decimal";
57
+ cast(value: unknown): string | null;
58
+ serialize(value: string | null): string | null;
59
+ deserialize(value: unknown): string | null;
60
+ }
61
+ declare class BooleanType implements Type<boolean> {
62
+ readonly type = "boolean";
63
+ cast(value: unknown): boolean | null;
64
+ serialize(value: boolean | null): boolean | null;
65
+ deserialize(value: unknown): boolean | null;
66
+ }
67
+ declare class DateType implements Type<Date> {
68
+ readonly type = "date";
69
+ cast(value: unknown): Date | null;
70
+ serialize(value: Date | null): string | null;
71
+ deserialize(value: unknown): Date | null;
72
+ equals(a: Date | null, b: Date | null): boolean;
73
+ }
74
+ declare class DateTimeType implements Type<Date> {
75
+ readonly type = "datetime";
76
+ cast(value: unknown): Date | null;
77
+ serialize(value: Date | null): string | null;
78
+ deserialize(value: unknown): Date | null;
79
+ equals(a: Date | null, b: Date | null): boolean;
80
+ }
81
+ declare class JSONType<T = unknown> implements Type<T> {
82
+ readonly type = "json";
83
+ cast(value: unknown): T | null;
84
+ serialize(value: T | null): string | null;
85
+ deserialize(value: unknown): T | null;
86
+ }
87
+ declare class BinaryType implements Type<Uint8Array> {
88
+ readonly type = "binary";
89
+ cast(value: unknown): Uint8Array | null;
90
+ serialize(value: Uint8Array | null): Uint8Array<ArrayBufferLike> | null;
91
+ deserialize(value: unknown): Uint8Array | null;
92
+ equals(a: Uint8Array | null, b: Uint8Array | null): boolean;
93
+ }
94
+ /**
95
+ * Pass-through type for unmapped or adapter-specific values. Used as the
96
+ * default when schema reflection finds a column type we don't know about.
97
+ */
98
+ declare class ValueType implements Type<unknown> {
99
+ readonly type = "value";
100
+ cast(value: unknown): unknown;
101
+ serialize(value: unknown): unknown;
102
+ deserialize(value: unknown): unknown;
103
+ }
104
+ /** Compare two values via the type's `equals` hook if present, else `===`. */
105
+ declare const valuesEqual: <T>(type: Type<T>, a: T | null, b: T | null) => boolean;
106
+ type Factory = () => Type;
107
+ /** Register a named type (factory invoked per `Type.lookup`). */
108
+ declare const registerType: (name: string, factory: Factory) => void;
109
+ /** Resolve a named type. Throws if unregistered. */
110
+ declare const lookupType: (name: string) => Type;
111
+ /** True when the named type has been registered. */
112
+ declare const hasType: (name: string) => boolean;
113
+
114
+ /**
115
+ * Per-instance attribute storage with dirty tracking.
116
+ *
117
+ * Two layers of values:
118
+ * - `original` — the value when the record was loaded from the DB (or
119
+ * last `save`/`commit`). Used to compute the dirty diff.
120
+ * - `current` — the working value, updated by every assignment.
121
+ *
122
+ * Dirty surface (mirrors Rails' `ActiveModel::Dirty`):
123
+ * - `changed()` — list of attribute names that differ from original
124
+ * - `changes()` — `{ name: [from, to] }` map of pending changes
125
+ * - `attributeChanged(name)` — boolean
126
+ * - `attributeWas(name)` — original value
127
+ * - `savedChanges()` — changes that were applied during the last save
128
+ * - `attributePreviouslyChanged(name)` — boolean (post-save introspection)
129
+ */
130
+
131
+ /** Declaration of a single attribute on a model. */
132
+ type AttributeDefinition = {
133
+ name: string;
134
+ type: Type;
135
+ /** Default applied when a record is new and the attribute is not assigned. */
136
+ default?: unknown;
137
+ };
138
+ declare class AttributeSet {
139
+ private readonly definitions;
140
+ /** Insertion-ordered list of attribute names, for stable iteration. */
141
+ private readonly names;
142
+ /** Register an attribute (or replace an existing one). */
143
+ define(definition: AttributeDefinition): void;
144
+ has(name: string): boolean;
145
+ get(name: string): AttributeDefinition | undefined;
146
+ keys(): string[];
147
+ /** Stable iteration of definitions in declaration order. */
148
+ [Symbol.iterator](): IterableIterator<AttributeDefinition>;
149
+ size(): number;
150
+ clone(): AttributeSet;
151
+ }
152
+ /** Per-instance attribute state — values + originals + last-save snapshot. */
153
+ declare class Attributes {
154
+ private readonly set;
155
+ private readonly current;
156
+ private readonly original;
157
+ private previousChanges;
158
+ private readonly accessedNames;
159
+ constructor(set: AttributeSet);
160
+ /** Hydrate attributes after loading from the DB. Skips dirty tracking. */
161
+ hydrate(raw: Record<string, unknown>): void;
162
+ /** Hydrate attributes for a brand-new record (applies defaults). */
163
+ hydrateDefaults(overrides?: Record<string, unknown>): void;
164
+ /** Read an attribute by name, returning the canonical (cast) value. */
165
+ read(name: string): unknown;
166
+ /** Names that have been read since hydration. Mirrors Rails' `accessed_attributes`. */
167
+ accessed(): string[];
168
+ /** Write an attribute by name. Type-casts before storing. */
169
+ write(name: string, value: unknown): void;
170
+ /**
171
+ * Explicitly mark `name` as having been mutated in place — without
172
+ * actually writing a new value. Mirrors Rails' `name_will_change!`
173
+ * which captures the current value into the original snapshot for
174
+ * later mutation detection.
175
+ */
176
+ willChange(name: string): void;
177
+ /** Was this attribute changed since the last commit? */
178
+ changed(name: string): boolean;
179
+ /** List of attribute names that differ from their originals. */
180
+ changedAttributes(): string[];
181
+ /** `{ name: [from, to] }` for every changed attribute. */
182
+ changes(): Record<string, [unknown, unknown]>;
183
+ /** Original value of `name` (current value if unchanged). */
184
+ was(name: string): unknown;
185
+ /** Snapshot for SAVE — return the canonical values for each attribute. */
186
+ toHash(): Record<string, unknown>;
187
+ /** Snapshot of only the dirty attributes (for UPDATE statements). */
188
+ dirtyHash(): Record<string, unknown>;
189
+ /** Serialized snapshot of all attributes (driver-ready values). */
190
+ serializedHash(): Record<string, unknown>;
191
+ /** Mark the current values as the new baseline. Captures previous changes. */
192
+ commit(): void;
193
+ /** Drop all pending and recorded changes — used by `clearChangesInformation`. */
194
+ clearChanges(): void;
195
+ /** Revert all pending changes. */
196
+ restore(): void;
197
+ /** Changes that were applied during the last `commit`. */
198
+ savedChanges(): Record<string, [unknown, unknown]>;
199
+ attributeSet(): AttributeSet;
200
+ }
201
+
202
+ /**
203
+ * Per-class callback chains for save/create/update/destroy/validation.
204
+ *
205
+ * Each chain holds an ordered list of callbacks. A `before` callback may
206
+ * abort the chain by returning `false`. `around` callbacks receive a
207
+ * `yield` function that runs the rest of the chain.
208
+ *
209
+ * The shape mirrors `ActiveSupport::Callbacks` enough to express Rails-y
210
+ * lifecycle hooks, without trying to be a general callback framework.
211
+ */
212
+ type CallbackKind = 'before' | 'after' | 'around';
213
+ type CallbackEvent = 'validation' | 'save' | 'create' | 'update' | 'destroy' | 'commit' | 'rollback' | 'initialize' | 'find' | 'touch';
214
+ type CallbackFn<T> = (record: T) => void | boolean | Promise<void | boolean>;
215
+ type AroundCallbackFn<T> = (record: T, run: () => Promise<void>) => Promise<void>;
216
+ /** Sentinel — throw inside a callback to cleanly halt the chain. */
217
+ declare class HaltError extends Error {
218
+ constructor();
219
+ }
220
+ /**
221
+ * Symbol-shaped sentinel callers can throw to halt the chain — mirrors
222
+ * Rails' `throw :abort` semantics. The chain swallows it and treats it
223
+ * as a `false` return from a `before_*` callback.
224
+ */
225
+ declare const ABORT_SENTINEL: unique symbol;
226
+ /** Convenience helper for chain implementations: convert "throw :abort" to a clean halt. */
227
+ declare const throwAbort: () => never;
228
+ declare class CallbackChain<T> {
229
+ private readonly chains;
230
+ /** Register a new callback under `event` of `kind`. */
231
+ add(event: CallbackEvent, kind: CallbackKind, fn: CallbackFn<T> | AroundCallbackFn<T>, options?: {
232
+ if?: (record: T) => boolean;
233
+ unless?: (record: T) => boolean;
234
+ on?: string | string[];
235
+ }): void;
236
+ /** Copy chains from a parent so subclasses inherit before extending. */
237
+ inheritFrom(parent: CallbackChain<T>): void;
238
+ /**
239
+ * Run the chain around `body`. Returns `false` when a `before` callback
240
+ * halts; otherwise returns the return value of `body`. Pass a `context`
241
+ * to filter callbacks registered with `on:` — primarily used for
242
+ * validation contexts (`create`/`update`) but available everywhere.
243
+ */
244
+ run(event: CallbackEvent, record: T, body: () => Promise<void | boolean>, context?: string): Promise<boolean>;
245
+ }
246
+
247
+ /**
248
+ * Error collection on a model instance. Mirrors `ActiveModel::Errors`
249
+ * minus i18n — message strings are stored verbatim. The full set of
250
+ * Rails behaviors we now support:
251
+ *
252
+ * - `add(attribute, typeOrMessage, options?)` where the second argument
253
+ * can be a known symbol-style type (e.g. `'blank'`, `'too_short'`)
254
+ * that maps to a default message string, or a free-form message.
255
+ * - `on(attribute)` returns the list of messages for an attribute.
256
+ * - `attributeNames` returns the unique attributes carrying an error.
257
+ * - `added(attribute, type?, options?)` predicate.
258
+ * - `where(attribute, type?, options?)` returns the matching entries.
259
+ * - `messagesFor` / `fullMessagesFor` with optional type filter.
260
+ * - `merge(other)` / `copy(other)`.
261
+ */
262
+ type ErrorEntry = {
263
+ attribute: string;
264
+ message: string;
265
+ /** Optional discriminator for the validator that produced this error. */
266
+ type?: string;
267
+ /** Optional structured payload, e.g. `{ count: 2 }` for length validators. */
268
+ options?: Record<string, unknown>;
269
+ };
270
+ /**
271
+ * Rich error object exposed by `errors.objects` / `errors.first` / iteration.
272
+ * Mirrors a subset of Rails' `ActiveModel::Error` — enough for callers
273
+ * who want to introspect (attribute / type / options / full message) rather
274
+ * than work with bare strings.
275
+ */
276
+ declare class ErrorObject {
277
+ private readonly entry;
278
+ constructor(entry: ErrorEntry);
279
+ get attribute(): string;
280
+ get message(): string;
281
+ get type(): string | undefined;
282
+ get options(): Record<string, unknown> | undefined;
283
+ fullMessage(): string;
284
+ /** Internal accessor for `Errors#import` — returns the wrapped entry. */
285
+ toEntry(): ErrorEntry;
286
+ }
287
+ /** Special attribute name for errors that aren't tied to a specific column. */
288
+ declare const BASE = "base";
289
+ type AddOptions = {
290
+ type?: string;
291
+ options?: Record<string, unknown>;
292
+ };
293
+ declare class Errors {
294
+ private readonly entries;
295
+ /**
296
+ * Record an error. The second argument can be:
297
+ * - a free-form message string (e.g. `errors.add('name', "can't be empty")`)
298
+ * - a known symbol-style type that resolves to a default message
299
+ * (e.g. `errors.add('name', 'blank')` -> "can't be blank")
300
+ *
301
+ * Pass extra interpolation values or a custom `type` via the third arg.
302
+ */
303
+ add(attribute: string, messageOrType?: string, options?: AddOptions & Record<string, unknown>): ErrorEntry;
304
+ /** True when there are no errors at all. */
305
+ get empty(): boolean;
306
+ /** True when at least one error has been recorded. */
307
+ get any(): boolean;
308
+ clear(): void;
309
+ delete(attribute: string, type?: string, matchOptions?: Record<string, unknown>): string[];
310
+ on(attribute: string): string[];
311
+ includes(attribute: string): boolean;
312
+ /** All `[attribute, message]` pairs, in insertion order. */
313
+ get messages(): Record<string, string[]>;
314
+ /** Distinct attributes that currently carry at least one error. */
315
+ get attributeNames(): string[];
316
+ /** Predicate: was an error matching the given attribute (and optional type / options) added? */
317
+ added(attribute: string, type?: string, matchOptions?: Record<string, unknown>): boolean;
318
+ /** Filter entries by attribute / type / options. */
319
+ where(attribute: string, type?: string, matchOptions?: Record<string, unknown>): ErrorEntry[];
320
+ /** Messages for a specific attribute, optionally filtered by type. */
321
+ messagesFor(attribute: string, type?: string): string[];
322
+ /** Human-readable strings like `"Name can't be blank"`. */
323
+ get fullMessages(): string[];
324
+ fullMessagesFor(attribute: string, type?: string): string[];
325
+ /** Standalone full-message formatter (no entries side effect). */
326
+ fullMessage(attribute: string, message: string): string;
327
+ /** Append every entry from `other` to this collection. */
328
+ merge(other: Errors): this;
329
+ /** Replace this collection's entries with copies of another's. */
330
+ copy(other: Errors): this;
331
+ /**
332
+ * Return a new `Errors` collection with the same entries. Subsequent
333
+ * modifications on either side don't affect the other.
334
+ */
335
+ dup(): Errors;
336
+ /** Rich `ErrorObject` instances for each entry, in insertion order. */
337
+ get objects(): ErrorObject[];
338
+ /** First error (rich object) or `null`. */
339
+ get first(): ErrorObject | null;
340
+ /**
341
+ * Structured details payload. Each attribute maps to a list of
342
+ * `{ error: type, ...options }` records. Mirrors Rails' `errors.details`.
343
+ */
344
+ get details(): Record<string, Array<{
345
+ error: string | undefined;
346
+ } & Record<string, unknown>>>;
347
+ /** Group rich error objects by attribute. */
348
+ groupByAttribute(): Record<string, ErrorObject[]>;
349
+ /** Indifferent indexer — `errors.get('name')` is equivalent to `errors.on('name')`. */
350
+ get(attribute: string): string[];
351
+ /**
352
+ * Remove duplicate entries — same attribute + message + type. Mirrors
353
+ * Rails' `errors.uniq!`.
354
+ */
355
+ uniq(): this;
356
+ /** Import a foreign `ErrorEntry` or `ErrorObject`, optionally overriding attribute/type. */
357
+ import(error: ErrorEntry | ErrorObject, overrides?: {
358
+ attribute?: string;
359
+ type?: string;
360
+ }): ErrorEntry;
361
+ /**
362
+ * Indifferent indexer — returns `errors.on(attribute)` via a Proxy
363
+ * pseudo-property, supporting both `errors.byAttribute('name')` and
364
+ * the bracket-access shape `(errors as any)['name']`. Mirrors Rails'
365
+ * `errors[:name]` / `errors['name']`. (We can't do real bracket access
366
+ * without wrapping every Errors instance in a Proxy; this method-style
367
+ * accessor is the idiomatic TS equivalent.)
368
+ */
369
+ byAttribute(attribute: string): string[];
370
+ /**
371
+ * Rails-style `as_json`. With `{ fullMessages: true }`, each entry is
372
+ * the humanized "Attribute msg" form. Default returns the same shape
373
+ * as `messages`.
374
+ */
375
+ asJson(options?: {
376
+ fullMessages?: boolean;
377
+ }): Record<string, string[]>;
378
+ /**
379
+ * `to_hash(true)` returns full-message variant; otherwise plain messages.
380
+ * Convenience shim over `asJson({ fullMessages })`.
381
+ */
382
+ toHash(fullMessages?: boolean): Record<string, string[]>;
383
+ /**
384
+ * `of_kind?` — true when an entry exists whose attribute and type-or-
385
+ * message both match. Mirrors Rails' `errors.of_kind?(:name, :blank)`.
386
+ */
387
+ ofKind(attribute: string, typeOrMessage?: string): boolean;
388
+ /** Debug-friendly string representation. */
389
+ inspect(): string;
390
+ get count(): number;
391
+ get size(): number;
392
+ [Symbol.iterator](): IterableIterator<ErrorEntry>;
393
+ toJSON(): Record<string, string[]>;
394
+ }
395
+
396
+ /**
397
+ * Built-in validators. Each validator implements `validate(record, errors)`.
398
+ * Adapter-level validators (e.g. uniqueness) live in active-record.
399
+ */
400
+
401
+ /**
402
+ * Optional context name passed to `Model#validate(context)`. Mirrors Rails'
403
+ * `valid?(:create)`/`valid?(:update)` — validators with `on` matching the
404
+ * context (or with no `on` at all) are evaluated; others are skipped.
405
+ */
406
+ type ValidationContext = string;
407
+ interface Validator<T = unknown> {
408
+ validate(record: T, errors: Errors, context?: ValidationContext): void | Promise<void>;
409
+ }
410
+ /** Common options shared across all validators. */
411
+ type ValidatorOptions<T> = {
412
+ /**
413
+ * Custom error message. Can be a string or a function that receives
414
+ * `(record, data)` like Rails' Proc messages, where `data` is
415
+ * `{ attribute, value, type }`.
416
+ */
417
+ message?: string | ((record: T, data: {
418
+ attribute: string;
419
+ value: unknown;
420
+ type?: string;
421
+ }) => string);
422
+ if?: (record: T) => boolean;
423
+ unless?: (record: T) => boolean;
424
+ allowNull?: boolean;
425
+ allowBlank?: boolean;
426
+ /** Limit the validator to one or more validation contexts (e.g. `'create'`). */
427
+ on?: ValidationContext | ValidationContext[];
428
+ /** Skip the validator when the validation context matches one of these. */
429
+ exceptOn?: ValidationContext | ValidationContext[];
430
+ /**
431
+ * When set, validator failures throw immediately instead of accumulating
432
+ * in `record.errors`. Pass `true` for a generic `StrictValidationFailed`
433
+ * or a custom Error subclass to use that instead. Mirrors Rails'
434
+ * `validates(..., strict: true)`.
435
+ */
436
+ strict?: boolean | (new (message: string) => Error);
437
+ };
438
+ /** Thrown when a validator marked `strict: true` fails. */
439
+ declare class StrictValidationFailed extends Error {
440
+ constructor(message: string);
441
+ }
442
+ declare class PresenceValidator<T> implements Validator<T> {
443
+ private readonly attribute;
444
+ private readonly options;
445
+ readonly kind = "presence";
446
+ readonly attributes: string[];
447
+ constructor(attribute: string, options?: ValidatorOptions<T>);
448
+ validate(record: T, errors: Errors, context?: ValidationContext): void;
449
+ }
450
+ declare class AbsenceValidator<T> implements Validator<T> {
451
+ private readonly attribute;
452
+ private readonly options;
453
+ readonly kind = "absence";
454
+ readonly attributes: string[];
455
+ constructor(attribute: string, options?: ValidatorOptions<T>);
456
+ validate(record: T, errors: Errors, context?: ValidationContext): void;
457
+ }
458
+ type LengthOptions<T> = ValidatorOptions<T> & {
459
+ minimum?: number;
460
+ maximum?: number;
461
+ is?: number;
462
+ in?: [number, number];
463
+ tooShort?: string;
464
+ tooLong?: string;
465
+ wrongLength?: string;
466
+ };
467
+ declare class LengthValidator<T> implements Validator<T> {
468
+ private readonly attribute;
469
+ private readonly options;
470
+ readonly kind = "length";
471
+ readonly attributes: string[];
472
+ constructor(attribute: string, options?: LengthOptions<T>);
473
+ validate(record: T, errors: Errors, context?: ValidationContext): void;
474
+ }
475
+ type FormatOptions<T> = ValidatorOptions<T> & {
476
+ with?: RegExp;
477
+ without?: RegExp;
478
+ };
479
+ declare class FormatValidator<T> implements Validator<T> {
480
+ private readonly attribute;
481
+ private readonly options;
482
+ readonly kind = "format";
483
+ readonly attributes: string[];
484
+ constructor(attribute: string, options: FormatOptions<T>);
485
+ validate(record: T, errors: Errors, context?: ValidationContext): void;
486
+ }
487
+ type InclusionOptions<T> = ValidatorOptions<T> & {
488
+ in: readonly unknown[];
489
+ };
490
+ declare class InclusionValidator<T> implements Validator<T> {
491
+ private readonly attribute;
492
+ private readonly options;
493
+ readonly kind = "inclusion";
494
+ readonly attributes: string[];
495
+ constructor(attribute: string, options: InclusionOptions<T>);
496
+ validate(record: T, errors: Errors, context?: ValidationContext): void;
497
+ }
498
+ type ExclusionOptions<T> = ValidatorOptions<T> & {
499
+ in: readonly unknown[];
500
+ };
501
+ declare class ExclusionValidator<T> implements Validator<T> {
502
+ private readonly attribute;
503
+ private readonly options;
504
+ readonly kind = "exclusion";
505
+ readonly attributes: string[];
506
+ constructor(attribute: string, options: ExclusionOptions<T>);
507
+ validate(record: T, errors: Errors, context?: ValidationContext): void;
508
+ }
509
+ type NumericalityOptions<T> = ValidatorOptions<T> & {
510
+ onlyInteger?: boolean;
511
+ greaterThan?: number;
512
+ greaterThanOrEqualTo?: number;
513
+ lessThan?: number;
514
+ lessThanOrEqualTo?: number;
515
+ equalTo?: number;
516
+ odd?: boolean;
517
+ even?: boolean;
518
+ };
519
+ declare class NumericalityValidator<T> implements Validator<T> {
520
+ private readonly attribute;
521
+ private readonly options;
522
+ readonly kind = "numericality";
523
+ readonly attributes: string[];
524
+ constructor(attribute: string, options?: NumericalityOptions<T>);
525
+ validate(record: T, errors: Errors, context?: ValidationContext): void;
526
+ }
527
+ type AcceptanceOptions<T> = ValidatorOptions<T> & {
528
+ accept?: readonly unknown[];
529
+ };
530
+ declare class AcceptanceValidator<T> implements Validator<T> {
531
+ private readonly attribute;
532
+ private readonly options;
533
+ readonly kind = "acceptance";
534
+ readonly attributes: string[];
535
+ constructor(attribute: string, options?: AcceptanceOptions<T>);
536
+ validate(record: T, errors: Errors, context?: ValidationContext): void;
537
+ }
538
+ type ConfirmationOptions<T> = ValidatorOptions<T> & {
539
+ caseSensitive?: boolean;
540
+ };
541
+ declare class ConfirmationValidator<T> implements Validator<T> {
542
+ private readonly attribute;
543
+ private readonly options;
544
+ readonly kind = "confirmation";
545
+ readonly attributes: string[];
546
+ constructor(attribute: string, options?: ConfirmationOptions<T>);
547
+ validate(record: T, errors: Errors, context?: ValidationContext): void;
548
+ }
549
+
550
+ /**
551
+ * `Model` is the active-model base — handles attribute storage, dirty
552
+ * tracking, validation, and callbacks. ActiveRecord's `Base` subclasses
553
+ * this to add persistence.
554
+ *
555
+ * Subclasses declare attributes with `static attribute(name, type)` or
556
+ * by populating the static `attributesSchema` (the AR layer does this
557
+ * automatically from `loadSchema`).
558
+ */
559
+
560
+ /** A type reference accepted by `Model.attribute` — either a name or a Type instance. */
561
+ type TypeRef = string | Type;
562
+ /** Thrown by `Model#validateOrThrow` (mirrors Rails' `ActiveModel::ValidationError`). */
563
+ declare class ValidationError extends Error {
564
+ record: Model;
565
+ constructor(record: Model);
566
+ }
567
+ declare class Model {
568
+ /** Per-instance attribute state, lazily initialized. */
569
+ protected _attributes: Attributes;
570
+ /** Per-instance error collection. */
571
+ readonly errors: Errors;
572
+ constructor(values?: Record<string, unknown>);
573
+ readAttribute(name: string): unknown;
574
+ writeAttribute(name: string, value: unknown): void;
575
+ /** Plain object snapshot. */
576
+ attributes(): Record<string, unknown>;
577
+ assignAttributes(values: Record<string, unknown>): this;
578
+ changed(): string[];
579
+ changes(): Record<string, [unknown, unknown]>;
580
+ attributeChanged(name: string): boolean;
581
+ attributeWas(name: string): unknown;
582
+ savedChanges(): Record<string, [unknown, unknown]>;
583
+ /**
584
+ * Tell the dirty tracker that `name` is about to be mutated in place,
585
+ * so subsequent reads detect it as a change. Mirrors Rails'
586
+ * `name_will_change!`.
587
+ */
588
+ willChange(name: string): void;
589
+ /**
590
+ * Revert pending changes. With no argument restores every changed
591
+ * attribute; with `names` restores only the listed ones. Mirrors
592
+ * Rails' `restore_attributes(['name'])`.
593
+ */
594
+ restoreAttributes(names?: string[]): void;
595
+ /** Reset both pending and last-saved changes — Rails' `clear_changes_information`. */
596
+ clearChangesInformation(): void;
597
+ validate(context?: ValidationContext): Promise<boolean>;
598
+ isValid(context?: ValidationContext): Promise<boolean>;
599
+ isInvalid(context?: ValidationContext): Promise<boolean>;
600
+ /** Mirrors Rails' `validate!`. Throws when invalid, returns true otherwise. */
601
+ validateOrThrow(context?: ValidationContext): Promise<boolean>;
602
+ toJSON(options?: {
603
+ except?: readonly string[];
604
+ only?: readonly string[];
605
+ }): Record<string, unknown>;
606
+ /**
607
+ * Return a new instance of the same class with the same attribute
608
+ * values. Mirrors Rails' `record.dup`. The AR layer overrides this
609
+ * to also clear the primary key so the duplicate looks like a fresh
610
+ * (unsaved) record.
611
+ */
612
+ dup(): this;
613
+ /** Register an attribute on this subclass. Returns the constructor for chaining. */
614
+ static attribute<This extends typeof Model>(this: This, name: string, type: TypeRef, options?: {
615
+ default?: unknown;
616
+ }): This;
617
+ /** Access the per-class `AttributeSet`. */
618
+ static attributesSchema<This extends typeof Model>(this: This): AttributeSet;
619
+ /** Register a validator instance. */
620
+ static validatesWith<This extends typeof Model>(this: This, validator: Validator<InstanceType<This>>): This;
621
+ /**
622
+ * Register a free-form block validator — `Klass.validate((record, errors) => ...)`.
623
+ * The function may be async and receives the same `(record, errors, context)`
624
+ * arguments every built-in validator does.
625
+ */
626
+ static validate<This extends typeof Model>(this: This, fn: (record: InstanceType<This>, errors: Errors, context?: ValidationContext) => void | Promise<void>, options?: ValidatorOptions<InstanceType<This>>): This;
627
+ /** All registered validators in declaration order (inherited + own). */
628
+ static validators<This extends typeof Model>(this: This): ReadonlyArray<Validator<InstanceType<This>>>;
629
+ /** Validators that target any of the given attribute names. */
630
+ static validatorsOn<This extends typeof Model>(this: This, ...attributes: string[]): ReadonlyArray<Validator<InstanceType<This>>>;
631
+ /** Reset all per-class validators. Rails' `clear_validators!`. */
632
+ static clearValidators<This extends typeof Model>(this: This): This;
633
+ /**
634
+ * Run a function once per attribute, accumulating errors. Mirrors Rails'
635
+ * `validates_each :a, :b do |record, attr, value| ... end`.
636
+ *
637
+ * Klass.validatesEach(['a', 'b'], (record, attr, value, errors) => {
638
+ * if (!ok(value)) errors.add(attr, 'bad');
639
+ * });
640
+ */
641
+ static validatesEach<This extends typeof Model>(this: This, attributes: readonly string[], fn: (record: InstanceType<This>, attribute: string, value: unknown, errors: Errors) => void | Promise<void>, options?: ValidatorOptions<InstanceType<This>>): This;
642
+ /** Add a presence validator. */
643
+ static validatesPresenceOf<This extends typeof Model>(this: This, attribute: string, options?: ValidatorOptions<InstanceType<This>>): This;
644
+ static validatesAbsenceOf<This extends typeof Model>(this: This, attribute: string, options?: ValidatorOptions<InstanceType<This>>): This;
645
+ static validatesLengthOf<This extends typeof Model>(this: This, attribute: string, options: LengthOptions<InstanceType<This>>): This;
646
+ static validatesFormatOf<This extends typeof Model>(this: This, attribute: string, options: FormatOptions<InstanceType<This>>): This;
647
+ static validatesInclusionOf<This extends typeof Model>(this: This, attribute: string, options: InclusionOptions<InstanceType<This>>): This;
648
+ static validatesExclusionOf<This extends typeof Model>(this: This, attribute: string, options: ExclusionOptions<InstanceType<This>>): This;
649
+ static validatesNumericalityOf<This extends typeof Model>(this: This, attribute: string, options?: NumericalityOptions<InstanceType<This>>): This;
650
+ static validatesAcceptanceOf<This extends typeof Model>(this: This, attribute: string, options?: AcceptanceOptions<InstanceType<This>>): This;
651
+ static validatesConfirmationOf<This extends typeof Model>(this: This, attribute: string, options?: ConfirmationOptions<InstanceType<This>>): This;
652
+ /**
653
+ * Sugar mirroring Rails' `validates :name, presence: true, length: { minimum: 2 }`.
654
+ */
655
+ static validates<This extends typeof Model>(this: This, attribute: string, rules: {
656
+ presence?: boolean | ValidatorOptions<InstanceType<This>>;
657
+ absence?: boolean | ValidatorOptions<InstanceType<This>>;
658
+ length?: LengthOptions<InstanceType<This>>;
659
+ format?: FormatOptions<InstanceType<This>>;
660
+ inclusion?: InclusionOptions<InstanceType<This>>;
661
+ exclusion?: ExclusionOptions<InstanceType<This>>;
662
+ numericality?: boolean | NumericalityOptions<InstanceType<This>>;
663
+ acceptance?: boolean | AcceptanceOptions<InstanceType<This>>;
664
+ confirmation?: boolean | ConfirmationOptions<InstanceType<This>>;
665
+ }): This;
666
+ /** Options that can be passed to any callback registration helper. */
667
+ static setCallback<This extends typeof Model>(this: This, event: CallbackEvent, kind: CallbackKind, fn: CallbackFn<InstanceType<This>> | AroundCallbackFn<InstanceType<This>>, options?: {
668
+ if?: (record: InstanceType<This>) => boolean;
669
+ unless?: (record: InstanceType<This>) => boolean;
670
+ on?: string | string[];
671
+ }): This;
672
+ static beforeValidation<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>, options?: {
673
+ on?: string | string[];
674
+ if?: (record: InstanceType<This>) => boolean;
675
+ unless?: (record: InstanceType<This>) => boolean;
676
+ }): This;
677
+ static afterValidation<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>, options?: {
678
+ on?: string | string[];
679
+ if?: (record: InstanceType<This>) => boolean;
680
+ unless?: (record: InstanceType<This>) => boolean;
681
+ }): This;
682
+ static beforeSave<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>): This;
683
+ static afterSave<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>): This;
684
+ static aroundSave<This extends typeof Model>(this: This, fn: AroundCallbackFn<InstanceType<This>>): This;
685
+ static beforeCreate<This extends typeof Model>(this: This, ...fns: CallbackFn<InstanceType<This>>[]): This;
686
+ static afterCreate<This extends typeof Model>(this: This, ...fns: CallbackFn<InstanceType<This>>[]): This;
687
+ static beforeUpdate<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>): This;
688
+ static afterUpdate<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>): This;
689
+ static beforeDestroy<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>): This;
690
+ static afterDestroy<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>): This;
691
+ /**
692
+ * Run `fn` after the outermost transaction surrounding this record's
693
+ * save / destroy commits. When the record is touched outside a
694
+ * transaction, the callback fires immediately after the save.
695
+ */
696
+ static afterCommit<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>, options?: {
697
+ on?: 'create' | 'update' | 'destroy' | Array<'create' | 'update' | 'destroy'>;
698
+ }): This;
699
+ /** Run `fn` when the surrounding transaction rolls back. */
700
+ static afterRollback<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>, options?: {
701
+ on?: 'create' | 'update' | 'destroy' | Array<'create' | 'update' | 'destroy'>;
702
+ }): This;
703
+ /** Run `fn` whenever a new instance is constructed. */
704
+ static afterInitialize<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>): This;
705
+ /** Run `fn` when a record is hydrated from the database. */
706
+ static afterFind<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>): This;
707
+ /** Run `fn` after `touch()`. */
708
+ static afterTouch<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>): This;
709
+ /** Run a registered callback chain — typically used by ActiveRecord persistence. */
710
+ static runCallbacks<This extends typeof Model>(this: This, event: CallbackEvent, record: InstanceType<This>, body: () => Promise<void | boolean>, context?: string): Promise<boolean>;
711
+ }
712
+
713
+ /**
714
+ * Tiny inflector — enough to convert class names into Rails-style table
715
+ * names (`User` -> `users`, `Person` -> `people`, `Octopus` -> `octopi`).
716
+ * Not a substitute for a full inflector (no `inflect.rb` here); add cases
717
+ * as needed.
718
+ */
719
+ declare const pluralize: (word: string) => string;
720
+ declare const underscore: (word: string) => string;
721
+ declare const camelize: (word: string, lower?: boolean) => string;
722
+ /** Convert a class name into a Rails-style table name (`UserAccount` -> `user_accounts`). */
723
+ declare const tableize: (className: string) => string;
724
+
725
+ /**
726
+ * `Name` — Rails-style `ActiveModel::Name`. Exposes singular/plural/
727
+ * element/collection/route_key/param_key/i18n_key/human/uncountable
728
+ * derivations for a class name. We compute these purely from the class
729
+ * name via the inflector — no i18n, no class introspection.
730
+ */
731
+ declare class Name {
732
+ /** The class itself (or its name, when called with a class-name string). */
733
+ readonly klass: {
734
+ name: string;
735
+ };
736
+ /** The fully-qualified class name as written. */
737
+ readonly name: string;
738
+ /** Snake-case version of the (possibly namespaced) class name. e.g. `Post::TrackBack` -> `post_track_back`. */
739
+ readonly singular: string;
740
+ /** Plural form of `singular`. e.g. `post_track_back` -> `post_track_backs`. */
741
+ readonly plural: string;
742
+ /** Last segment, snake-cased. e.g. `Post::TrackBack` -> `track_back`. */
743
+ readonly element: string;
744
+ /** Forward-slash-joined collection name. e.g. `Post::TrackBack` -> `post/track_backs`. */
745
+ readonly collection: string;
746
+ /** Rails routing helper — plural element with optional namespace prefix. */
747
+ readonly route_key: string;
748
+ /** `singular` minus internal namespace separators. */
749
+ readonly param_key: string;
750
+ /** Slash-joined namespace key. e.g. `Post::TrackBack` -> `post/track_back`. */
751
+ readonly i18n_key: string;
752
+ /** Human-friendly element name. e.g. `track_back` -> `Track back`. */
753
+ readonly human: string;
754
+ /**
755
+ * Build a `Name` from a class (uses `klass.name`) or directly from a
756
+ * Ruby-style namespaced string ("Post::TrackBack").
757
+ */
758
+ constructor(klassOrName: string | {
759
+ name: string;
760
+ });
761
+ /** Whether the singular form is uncountable (plural === singular). */
762
+ get uncountable(): boolean;
763
+ toString(): string;
764
+ }
765
+
766
+ export { ABORT_SENTINEL, AbsenceValidator, type AcceptanceOptions, AcceptanceValidator, type AroundCallbackFn, type AttributeDefinition, AttributeSet, Attributes, BASE, BigIntType, BinaryType, BooleanType, CallbackChain, type CallbackEvent, type CallbackFn, type CallbackKind, type ConfirmationOptions, ConfirmationValidator, DateTimeType, DateType, DecimalType, type ErrorEntry, ErrorObject, Errors, type ExclusionOptions, ExclusionValidator, FloatType, type FormatOptions, FormatValidator, HaltError, type InclusionOptions, InclusionValidator, IntegerType, JSONType, type LengthOptions, LengthValidator, Model, Name, type NumericalityOptions, NumericalityValidator, PresenceValidator, StrictValidationFailed, StringType, type Type, type TypeRef, type ValidationContext, ValidationError, type Validator, type ValidatorOptions, ValueType, camelize, hasType, lookupType, pluralize, registerType, tableize, throwAbort, underscore, valuesEqual };