@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.
- package/dist/index.d.ts +766 -0
- package/dist/index.js +1808 -0
- package/dist/index.js.map +1 -0
- package/package.json +16 -7
- package/src/AttributeSet.ts +0 -221
- package/src/Callbacks.ts +0 -158
- package/src/Errors.ts +0 -398
- package/src/Model.ts +0 -546
- package/src/Name.ts +0 -69
- package/src/Type.ts +0 -320
- package/src/Validator.ts +0 -371
- package/src/index.ts +0 -59
- package/src/inflector.ts +0 -74
package/src/Model.ts
DELETED
|
@@ -1,546 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `Model` is the active-model base — handles attribute storage, dirty
|
|
3
|
-
* tracking, validation, and callbacks. ActiveRecord's `Base` subclasses
|
|
4
|
-
* this to add persistence.
|
|
5
|
-
*
|
|
6
|
-
* Subclasses declare attributes with `static attribute(name, type)` or
|
|
7
|
-
* by populating the static `attributesSchema` (the AR layer does this
|
|
8
|
-
* automatically from `loadSchema`).
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { Attributes, AttributeSet } from './AttributeSet';
|
|
12
|
-
import { CallbackChain, type CallbackEvent, type CallbackFn, type AroundCallbackFn, type CallbackKind } from './Callbacks';
|
|
13
|
-
import { Errors } from './Errors';
|
|
14
|
-
import { lookupType, type Type } from './Type';
|
|
15
|
-
import {
|
|
16
|
-
AcceptanceValidator,
|
|
17
|
-
type AcceptanceOptions,
|
|
18
|
-
AbsenceValidator,
|
|
19
|
-
BlockValidator,
|
|
20
|
-
ConfirmationValidator,
|
|
21
|
-
type ConfirmationOptions,
|
|
22
|
-
ExclusionValidator,
|
|
23
|
-
type ExclusionOptions,
|
|
24
|
-
FormatValidator,
|
|
25
|
-
type FormatOptions,
|
|
26
|
-
InclusionValidator,
|
|
27
|
-
type InclusionOptions,
|
|
28
|
-
LengthValidator,
|
|
29
|
-
type LengthOptions,
|
|
30
|
-
NumericalityValidator,
|
|
31
|
-
type NumericalityOptions,
|
|
32
|
-
PresenceValidator,
|
|
33
|
-
type ValidationContext,
|
|
34
|
-
type Validator,
|
|
35
|
-
type ValidatorOptions,
|
|
36
|
-
} from './Validator';
|
|
37
|
-
|
|
38
|
-
/** A type reference accepted by `Model.attribute` — either a name or a Type instance. */
|
|
39
|
-
export type TypeRef = string | Type;
|
|
40
|
-
|
|
41
|
-
/** Thrown by `Model#validateOrThrow` (mirrors Rails' `ActiveModel::ValidationError`). */
|
|
42
|
-
export class ValidationError extends Error {
|
|
43
|
-
constructor(public record: Model) {
|
|
44
|
-
super(`Validation failed: ${record.errors.fullMessages.join(', ')}`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const resolveType = (ref: TypeRef): Type => (typeof ref === 'string' ? lookupType(ref) : ref);
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Hidden registry attached to each Model subclass. Stored as a non-
|
|
52
|
-
* enumerable property keyed by a symbol so it doesn't leak into the
|
|
53
|
-
* subclass shape — but discoverable from any prototype.
|
|
54
|
-
*/
|
|
55
|
-
const REGISTRY = Symbol.for('@active-record-ts/active-model:registry');
|
|
56
|
-
|
|
57
|
-
type Registry<T extends Model> = {
|
|
58
|
-
attributeSet: AttributeSet;
|
|
59
|
-
validators: Validator<T>[];
|
|
60
|
-
callbacks: CallbackChain<T>;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const getRegistry = <T extends Model>(ctor: typeof Model): Registry<T> => {
|
|
64
|
-
// biome-ignore lint/suspicious/noExplicitAny: registry is constructor-owned
|
|
65
|
-
const own = (ctor as any)[REGISTRY] as Registry<T> | undefined;
|
|
66
|
-
if (own && Object.prototype.hasOwnProperty.call(ctor, REGISTRY)) return own;
|
|
67
|
-
// Walk the prototype chain to inherit then copy down.
|
|
68
|
-
const parent = Object.getPrototypeOf(ctor) as typeof Model | null;
|
|
69
|
-
const parentReg = parent && parent !== Function.prototype ? getRegistry<T>(parent) : null;
|
|
70
|
-
const fresh: Registry<T> = {
|
|
71
|
-
attributeSet: parentReg ? parentReg.attributeSet.clone() : new AttributeSet(),
|
|
72
|
-
validators: parentReg ? [...parentReg.validators] : [],
|
|
73
|
-
callbacks: new CallbackChain<T>(),
|
|
74
|
-
};
|
|
75
|
-
if (parentReg) fresh.callbacks.inheritFrom(parentReg.callbacks);
|
|
76
|
-
Object.defineProperty(ctor, REGISTRY, { value: fresh, enumerable: false, configurable: true, writable: false });
|
|
77
|
-
return fresh;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Camelize a Rails-style snake_case name into a method-friendly suffix
|
|
82
|
-
* (e.g. `'first_name'` -> `'FirstName'`). Used to derive per-attribute
|
|
83
|
-
* dirty helper names like `firstNameChanged()`.
|
|
84
|
-
*/
|
|
85
|
-
const camelizeSuffix = (name: string): string =>
|
|
86
|
-
name.replace(/(?:^|[_-])([a-z0-9])/gi, (_, c: string) => c.toUpperCase()).replace(/[^A-Za-z0-9]/g, '');
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Install per-attribute dirty helpers on the prototype: `nameChanged()`,
|
|
90
|
-
* `nameWas()`, `nameChange()`, `restoreName()`, `namePreviouslyChanged()`,
|
|
91
|
-
* `namePreviousChange()`. Mirrors a slice of `ActiveModel::Dirty`'s
|
|
92
|
-
* generated methods.
|
|
93
|
-
*/
|
|
94
|
-
const definePerAttributeDirty = (target: typeof Model, names: string[]): void => {
|
|
95
|
-
for (const name of names) {
|
|
96
|
-
const suffix = camelizeSuffix(name);
|
|
97
|
-
const camelName = lowerFirst(suffix);
|
|
98
|
-
const helpers: Record<string, (this: Model, ...args: unknown[]) => unknown> = {
|
|
99
|
-
[`${camelName}Changed`](this: Model) {
|
|
100
|
-
return this.attributeChanged(name);
|
|
101
|
-
},
|
|
102
|
-
[`${camelName}Was`](this: Model) {
|
|
103
|
-
return this.attributeWas(name);
|
|
104
|
-
},
|
|
105
|
-
[`${camelName}Change`](this: Model): [unknown, unknown] | null {
|
|
106
|
-
if (!this.attributeChanged(name)) return null;
|
|
107
|
-
return [this.attributeWas(name), this.readAttribute(name)];
|
|
108
|
-
},
|
|
109
|
-
[`restore${suffix}`](this: Model) {
|
|
110
|
-
this.writeAttribute(name, this.attributeWas(name));
|
|
111
|
-
},
|
|
112
|
-
[`${camelName}WillChange`](this: Model) {
|
|
113
|
-
this.willChange(name);
|
|
114
|
-
},
|
|
115
|
-
[`${camelName}PreviouslyChanged`](this: Model): boolean {
|
|
116
|
-
return Object.prototype.hasOwnProperty.call(this.savedChanges(), name);
|
|
117
|
-
},
|
|
118
|
-
[`${camelName}PreviousChange`](this: Model): [unknown, unknown] | null {
|
|
119
|
-
const saved = this.savedChanges();
|
|
120
|
-
return Object.prototype.hasOwnProperty.call(saved, name) ? saved[name]! : null;
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
for (const [methodName, fn] of Object.entries(helpers)) {
|
|
124
|
-
if (Object.prototype.hasOwnProperty.call(target.prototype, methodName)) continue;
|
|
125
|
-
Object.defineProperty(target.prototype, methodName, {
|
|
126
|
-
configurable: true,
|
|
127
|
-
enumerable: false,
|
|
128
|
-
writable: true,
|
|
129
|
-
value: fn,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const lowerFirst = (s: string): string => (s.length === 0 ? s : s.charAt(0).toLowerCase() + s.slice(1));
|
|
136
|
-
|
|
137
|
-
/** Accessor proxy installed on subclass prototypes so `record.name` reads `attributes`. */
|
|
138
|
-
const defineAccessors = (target: typeof Model, names: string[]): void => {
|
|
139
|
-
for (const name of names) {
|
|
140
|
-
if (Object.prototype.hasOwnProperty.call(target.prototype, name)) continue;
|
|
141
|
-
// Don't shadow an existing getter/setter inherited from an ancestor —
|
|
142
|
-
// e.g. `Base#id` is a composite-aware getter on Base.prototype and
|
|
143
|
-
// we shouldn't override it with a per-attribute accessor that always
|
|
144
|
-
// reads the single `id` column.
|
|
145
|
-
let proto: object | null = Object.getPrototypeOf(target.prototype);
|
|
146
|
-
let inheritedAccessor = false;
|
|
147
|
-
while (proto) {
|
|
148
|
-
const desc = Object.getOwnPropertyDescriptor(proto, name);
|
|
149
|
-
if (desc && (desc.get || desc.set)) { inheritedAccessor = true; break; }
|
|
150
|
-
proto = Object.getPrototypeOf(proto);
|
|
151
|
-
}
|
|
152
|
-
if (inheritedAccessor) continue;
|
|
153
|
-
Object.defineProperty(target.prototype, name, {
|
|
154
|
-
configurable: true,
|
|
155
|
-
enumerable: true,
|
|
156
|
-
get(this: Model) {
|
|
157
|
-
return this.readAttribute(name);
|
|
158
|
-
},
|
|
159
|
-
set(this: Model, value: unknown) {
|
|
160
|
-
this.writeAttribute(name, value);
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
export class Model {
|
|
167
|
-
/** Per-instance attribute state, lazily initialized. */
|
|
168
|
-
protected _attributes!: Attributes;
|
|
169
|
-
/** Per-instance error collection. */
|
|
170
|
-
public readonly errors: Errors = new Errors();
|
|
171
|
-
|
|
172
|
-
// biome-ignore lint/complexity/noBannedTypes: constructor body sets up attributes uniformly
|
|
173
|
-
constructor(values: Record<string, unknown> = {}) {
|
|
174
|
-
const ctor = this.constructor as typeof Model;
|
|
175
|
-
const reg = getRegistry<this>(ctor);
|
|
176
|
-
this._attributes = new Attributes(reg.attributeSet);
|
|
177
|
-
this._attributes.hydrateDefaults(values);
|
|
178
|
-
const names = reg.attributeSet.keys();
|
|
179
|
-
defineAccessors(ctor, names);
|
|
180
|
-
definePerAttributeDirty(ctor, names);
|
|
181
|
-
// Fire after_initialize callbacks. We use the synchronous chain via
|
|
182
|
-
// a promise so async hooks still run, but constructors can't await —
|
|
183
|
-
// any errors will surface as unhandled rejections, which matches
|
|
184
|
-
// Rails' "don't put expensive logic in after_initialize" expectation.
|
|
185
|
-
void reg.callbacks.run('initialize', this, async () => { /* body */ });
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ──────────────────────────── attribute IO ────────────────────────────
|
|
189
|
-
|
|
190
|
-
readAttribute(name: string): unknown {
|
|
191
|
-
return this._attributes.read(name);
|
|
192
|
-
}
|
|
193
|
-
writeAttribute(name: string, value: unknown): void {
|
|
194
|
-
this._attributes.write(name, value);
|
|
195
|
-
}
|
|
196
|
-
/** Plain object snapshot. */
|
|
197
|
-
attributes(): Record<string, unknown> {
|
|
198
|
-
return this._attributes.toHash();
|
|
199
|
-
}
|
|
200
|
-
assignAttributes(values: Record<string, unknown>): this {
|
|
201
|
-
for (const [name, value] of Object.entries(values)) this.writeAttribute(name, value);
|
|
202
|
-
return this;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// ──────────────────────────── dirty tracking ────────────────────────────
|
|
206
|
-
|
|
207
|
-
changed(): string[] {
|
|
208
|
-
return this._attributes.changedAttributes();
|
|
209
|
-
}
|
|
210
|
-
changes(): Record<string, [unknown, unknown]> {
|
|
211
|
-
return this._attributes.changes();
|
|
212
|
-
}
|
|
213
|
-
attributeChanged(name: string): boolean {
|
|
214
|
-
return this._attributes.changed(name);
|
|
215
|
-
}
|
|
216
|
-
attributeWas(name: string): unknown {
|
|
217
|
-
return this._attributes.was(name);
|
|
218
|
-
}
|
|
219
|
-
savedChanges(): Record<string, [unknown, unknown]> {
|
|
220
|
-
return this._attributes.savedChanges();
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Tell the dirty tracker that `name` is about to be mutated in place,
|
|
224
|
-
* so subsequent reads detect it as a change. Mirrors Rails'
|
|
225
|
-
* `name_will_change!`.
|
|
226
|
-
*/
|
|
227
|
-
willChange(name: string): void {
|
|
228
|
-
this._attributes.willChange(name);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Revert pending changes. With no argument restores every changed
|
|
233
|
-
* attribute; with `names` restores only the listed ones. Mirrors
|
|
234
|
-
* Rails' `restore_attributes(['name'])`.
|
|
235
|
-
*/
|
|
236
|
-
restoreAttributes(names?: string[]): void {
|
|
237
|
-
if (!names) return this._attributes.restore();
|
|
238
|
-
for (const name of names) {
|
|
239
|
-
this._attributes.write(name, this._attributes.was(name));
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
/** Reset both pending and last-saved changes — Rails' `clear_changes_information`. */
|
|
243
|
-
clearChangesInformation(): void {
|
|
244
|
-
this._attributes.clearChanges();
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// ──────────────────────────── validation ────────────────────────────
|
|
248
|
-
|
|
249
|
-
async validate(context?: ValidationContext): Promise<boolean> {
|
|
250
|
-
this.errors.clear();
|
|
251
|
-
const ctor = this.constructor as typeof Model;
|
|
252
|
-
const reg = getRegistry<this>(ctor);
|
|
253
|
-
await reg.callbacks.run('validation', this, async () => {
|
|
254
|
-
for (const v of reg.validators) await v.validate(this, this.errors, context);
|
|
255
|
-
}, context);
|
|
256
|
-
return this.errors.empty;
|
|
257
|
-
}
|
|
258
|
-
async isValid(context?: ValidationContext): Promise<boolean> {
|
|
259
|
-
return this.validate(context);
|
|
260
|
-
}
|
|
261
|
-
async isInvalid(context?: ValidationContext): Promise<boolean> {
|
|
262
|
-
return !(await this.validate(context));
|
|
263
|
-
}
|
|
264
|
-
/** Mirrors Rails' `validate!`. Throws when invalid, returns true otherwise. */
|
|
265
|
-
async validateOrThrow(context?: ValidationContext): Promise<boolean> {
|
|
266
|
-
const ok = await this.validate(context);
|
|
267
|
-
if (!ok) throw new ValidationError(this);
|
|
268
|
-
return true;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// ──────────────────────────── helpers ────────────────────────────
|
|
272
|
-
|
|
273
|
-
toJSON(options: { except?: readonly string[]; only?: readonly string[] } = {}): Record<string, unknown> {
|
|
274
|
-
const all = this.attributes();
|
|
275
|
-
if (options.only) {
|
|
276
|
-
const out: Record<string, unknown> = {};
|
|
277
|
-
for (const name of options.only) if (name in all) out[name] = all[name];
|
|
278
|
-
return out;
|
|
279
|
-
}
|
|
280
|
-
if (options.except) {
|
|
281
|
-
const out: Record<string, unknown> = {};
|
|
282
|
-
const exclude = new Set(options.except);
|
|
283
|
-
for (const [name, value] of Object.entries(all)) if (!exclude.has(name)) out[name] = value;
|
|
284
|
-
return out;
|
|
285
|
-
}
|
|
286
|
-
return all;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Return a new instance of the same class with the same attribute
|
|
291
|
-
* values. Mirrors Rails' `record.dup`. The AR layer overrides this
|
|
292
|
-
* to also clear the primary key so the duplicate looks like a fresh
|
|
293
|
-
* (unsaved) record.
|
|
294
|
-
*/
|
|
295
|
-
dup(): this {
|
|
296
|
-
const ctor = this.constructor as new () => this;
|
|
297
|
-
const copy = new ctor();
|
|
298
|
-
// biome-ignore lint/suspicious/noExplicitAny: protected hydrate
|
|
299
|
-
(copy as any)._attributes.hydrate(this.attributes());
|
|
300
|
-
return copy;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// ──────────────────────────── class-side configuration ────────────────────────────
|
|
304
|
-
|
|
305
|
-
/** Register an attribute on this subclass. Returns the constructor for chaining. */
|
|
306
|
-
static attribute<This extends typeof Model>(this: This, name: string, type: TypeRef, options?: { default?: unknown }): This {
|
|
307
|
-
const reg = getRegistry(this);
|
|
308
|
-
reg.attributeSet.define({ name, type: resolveType(type), default: options?.default });
|
|
309
|
-
defineAccessors(this, [name]);
|
|
310
|
-
definePerAttributeDirty(this, [name]);
|
|
311
|
-
return this;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/** Access the per-class `AttributeSet`. */
|
|
315
|
-
static attributesSchema<This extends typeof Model>(this: This): AttributeSet {
|
|
316
|
-
return getRegistry(this).attributeSet;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/** Register a validator instance. */
|
|
320
|
-
static validatesWith<This extends typeof Model>(this: This, validator: Validator<InstanceType<This>>): This {
|
|
321
|
-
getRegistry(this).validators.push(validator as Validator<Model>);
|
|
322
|
-
return this;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Register a free-form block validator — `Klass.validate((record, errors) => ...)`.
|
|
327
|
-
* The function may be async and receives the same `(record, errors, context)`
|
|
328
|
-
* arguments every built-in validator does.
|
|
329
|
-
*/
|
|
330
|
-
static validate<This extends typeof Model>(
|
|
331
|
-
this: This,
|
|
332
|
-
fn: (record: InstanceType<This>, errors: Errors, context?: ValidationContext) => void | Promise<void>,
|
|
333
|
-
options: ValidatorOptions<InstanceType<This>> = {},
|
|
334
|
-
): This {
|
|
335
|
-
return this.validatesWith(new BlockValidator(fn, options));
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/** All registered validators in declaration order (inherited + own). */
|
|
339
|
-
static validators<This extends typeof Model>(this: This): ReadonlyArray<Validator<InstanceType<This>>> {
|
|
340
|
-
return getRegistry(this).validators as unknown as ReadonlyArray<Validator<InstanceType<This>>>;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/** Validators that target any of the given attribute names. */
|
|
344
|
-
static validatorsOn<This extends typeof Model>(this: This, ...attributes: string[]): ReadonlyArray<Validator<InstanceType<This>>> {
|
|
345
|
-
const wanted = new Set(attributes);
|
|
346
|
-
return this.validators().filter((v) => {
|
|
347
|
-
const attrs = (v as unknown as { attributes?: readonly string[] }).attributes;
|
|
348
|
-
if (!attrs) return false;
|
|
349
|
-
for (const a of attrs) if (wanted.has(a)) return true;
|
|
350
|
-
return false;
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/** Reset all per-class validators. Rails' `clear_validators!`. */
|
|
355
|
-
static clearValidators<This extends typeof Model>(this: This): This {
|
|
356
|
-
getRegistry(this).validators.length = 0;
|
|
357
|
-
return this;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Run a function once per attribute, accumulating errors. Mirrors Rails'
|
|
362
|
-
* `validates_each :a, :b do |record, attr, value| ... end`.
|
|
363
|
-
*
|
|
364
|
-
* Klass.validatesEach(['a', 'b'], (record, attr, value, errors) => {
|
|
365
|
-
* if (!ok(value)) errors.add(attr, 'bad');
|
|
366
|
-
* });
|
|
367
|
-
*/
|
|
368
|
-
static validatesEach<This extends typeof Model>(
|
|
369
|
-
this: This,
|
|
370
|
-
attributes: readonly string[],
|
|
371
|
-
fn: (record: InstanceType<This>, attribute: string, value: unknown, errors: Errors) => void | Promise<void>,
|
|
372
|
-
options: ValidatorOptions<InstanceType<This>> = {},
|
|
373
|
-
): This {
|
|
374
|
-
return this.validate(async (record, errors) => {
|
|
375
|
-
for (const attr of attributes) {
|
|
376
|
-
const value = (record as unknown as Record<string, unknown>)[attr];
|
|
377
|
-
await fn(record, attr, value, errors);
|
|
378
|
-
}
|
|
379
|
-
}, options);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/** Add a presence validator. */
|
|
383
|
-
static validatesPresenceOf<This extends typeof Model>(this: This, attribute: string, options: ValidatorOptions<InstanceType<This>> = {}): This {
|
|
384
|
-
return this.validatesWith(new PresenceValidator(attribute, options));
|
|
385
|
-
}
|
|
386
|
-
static validatesAbsenceOf<This extends typeof Model>(this: This, attribute: string, options: ValidatorOptions<InstanceType<This>> = {}): This {
|
|
387
|
-
return this.validatesWith(new AbsenceValidator(attribute, options));
|
|
388
|
-
}
|
|
389
|
-
static validatesLengthOf<This extends typeof Model>(this: This, attribute: string, options: LengthOptions<InstanceType<This>>): This {
|
|
390
|
-
return this.validatesWith(new LengthValidator(attribute, options));
|
|
391
|
-
}
|
|
392
|
-
static validatesFormatOf<This extends typeof Model>(this: This, attribute: string, options: FormatOptions<InstanceType<This>>): This {
|
|
393
|
-
return this.validatesWith(new FormatValidator(attribute, options));
|
|
394
|
-
}
|
|
395
|
-
static validatesInclusionOf<This extends typeof Model>(this: This, attribute: string, options: InclusionOptions<InstanceType<This>>): This {
|
|
396
|
-
return this.validatesWith(new InclusionValidator(attribute, options));
|
|
397
|
-
}
|
|
398
|
-
static validatesExclusionOf<This extends typeof Model>(this: This, attribute: string, options: ExclusionOptions<InstanceType<This>>): This {
|
|
399
|
-
return this.validatesWith(new ExclusionValidator(attribute, options));
|
|
400
|
-
}
|
|
401
|
-
static validatesNumericalityOf<This extends typeof Model>(this: This, attribute: string, options: NumericalityOptions<InstanceType<This>> = {}): This {
|
|
402
|
-
return this.validatesWith(new NumericalityValidator(attribute, options));
|
|
403
|
-
}
|
|
404
|
-
static validatesAcceptanceOf<This extends typeof Model>(this: This, attribute: string, options: AcceptanceOptions<InstanceType<This>> = {}): This {
|
|
405
|
-
return this.validatesWith(new AcceptanceValidator(attribute, options));
|
|
406
|
-
}
|
|
407
|
-
static validatesConfirmationOf<This extends typeof Model>(this: This, attribute: string, options: ConfirmationOptions<InstanceType<This>> = {}): This {
|
|
408
|
-
return this.validatesWith(new ConfirmationValidator(attribute, options));
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Sugar mirroring Rails' `validates :name, presence: true, length: { minimum: 2 }`.
|
|
413
|
-
*/
|
|
414
|
-
static validates<This extends typeof Model>(this: This, attribute: string, rules: {
|
|
415
|
-
presence?: boolean | ValidatorOptions<InstanceType<This>>;
|
|
416
|
-
absence?: boolean | ValidatorOptions<InstanceType<This>>;
|
|
417
|
-
length?: LengthOptions<InstanceType<This>>;
|
|
418
|
-
format?: FormatOptions<InstanceType<This>>;
|
|
419
|
-
inclusion?: InclusionOptions<InstanceType<This>>;
|
|
420
|
-
exclusion?: ExclusionOptions<InstanceType<This>>;
|
|
421
|
-
numericality?: boolean | NumericalityOptions<InstanceType<This>>;
|
|
422
|
-
acceptance?: boolean | AcceptanceOptions<InstanceType<This>>;
|
|
423
|
-
confirmation?: boolean | ConfirmationOptions<InstanceType<This>>;
|
|
424
|
-
}): This {
|
|
425
|
-
if (rules.presence) this.validatesPresenceOf(attribute, rules.presence === true ? {} : rules.presence);
|
|
426
|
-
if (rules.absence) this.validatesAbsenceOf(attribute, rules.absence === true ? {} : rules.absence);
|
|
427
|
-
if (rules.length) this.validatesLengthOf(attribute, rules.length);
|
|
428
|
-
if (rules.format) this.validatesFormatOf(attribute, rules.format);
|
|
429
|
-
if (rules.inclusion) this.validatesInclusionOf(attribute, rules.inclusion);
|
|
430
|
-
if (rules.exclusion) this.validatesExclusionOf(attribute, rules.exclusion);
|
|
431
|
-
if (rules.numericality) this.validatesNumericalityOf(attribute, rules.numericality === true ? {} : rules.numericality);
|
|
432
|
-
if (rules.acceptance) this.validatesAcceptanceOf(attribute, rules.acceptance === true ? {} : rules.acceptance);
|
|
433
|
-
if (rules.confirmation) this.validatesConfirmationOf(attribute, rules.confirmation === true ? {} : rules.confirmation);
|
|
434
|
-
return this;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// ──────────────────────────── callbacks ────────────────────────────
|
|
438
|
-
|
|
439
|
-
/** Options that can be passed to any callback registration helper. */
|
|
440
|
-
static setCallback<This extends typeof Model>(
|
|
441
|
-
this: This,
|
|
442
|
-
event: CallbackEvent,
|
|
443
|
-
kind: CallbackKind,
|
|
444
|
-
fn: CallbackFn<InstanceType<This>> | AroundCallbackFn<InstanceType<This>>,
|
|
445
|
-
options?: {
|
|
446
|
-
if?: (record: InstanceType<This>) => boolean;
|
|
447
|
-
unless?: (record: InstanceType<This>) => boolean;
|
|
448
|
-
on?: string | string[];
|
|
449
|
-
},
|
|
450
|
-
): This {
|
|
451
|
-
getRegistry(this).callbacks.add(event, kind, fn as never, options as never);
|
|
452
|
-
return this;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
static beforeValidation<This extends typeof Model>(
|
|
456
|
-
this: This,
|
|
457
|
-
fn: CallbackFn<InstanceType<This>>,
|
|
458
|
-
options?: { on?: string | string[]; if?: (record: InstanceType<This>) => boolean; unless?: (record: InstanceType<This>) => boolean },
|
|
459
|
-
): This {
|
|
460
|
-
return this.setCallback('validation', 'before', fn, options);
|
|
461
|
-
}
|
|
462
|
-
static afterValidation<This extends typeof Model>(
|
|
463
|
-
this: This,
|
|
464
|
-
fn: CallbackFn<InstanceType<This>>,
|
|
465
|
-
options?: { on?: string | string[]; if?: (record: InstanceType<This>) => boolean; unless?: (record: InstanceType<This>) => boolean },
|
|
466
|
-
): This {
|
|
467
|
-
return this.setCallback('validation', 'after', fn, options);
|
|
468
|
-
}
|
|
469
|
-
static beforeSave<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>): This {
|
|
470
|
-
return this.setCallback('save', 'before', fn);
|
|
471
|
-
}
|
|
472
|
-
static afterSave<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>): This {
|
|
473
|
-
return this.setCallback('save', 'after', fn);
|
|
474
|
-
}
|
|
475
|
-
static aroundSave<This extends typeof Model>(this: This, fn: AroundCallbackFn<InstanceType<This>>): This {
|
|
476
|
-
return this.setCallback('save', 'around', fn);
|
|
477
|
-
}
|
|
478
|
-
static beforeCreate<This extends typeof Model>(this: This, ...fns: CallbackFn<InstanceType<This>>[]): This {
|
|
479
|
-
for (const fn of fns) this.setCallback('create', 'before', fn);
|
|
480
|
-
return this;
|
|
481
|
-
}
|
|
482
|
-
static afterCreate<This extends typeof Model>(this: This, ...fns: CallbackFn<InstanceType<This>>[]): This {
|
|
483
|
-
for (const fn of fns) this.setCallback('create', 'after', fn);
|
|
484
|
-
return this;
|
|
485
|
-
}
|
|
486
|
-
static beforeUpdate<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>): This {
|
|
487
|
-
return this.setCallback('update', 'before', fn);
|
|
488
|
-
}
|
|
489
|
-
static afterUpdate<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>): This {
|
|
490
|
-
return this.setCallback('update', 'after', fn);
|
|
491
|
-
}
|
|
492
|
-
static beforeDestroy<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>): This {
|
|
493
|
-
return this.setCallback('destroy', 'before', fn);
|
|
494
|
-
}
|
|
495
|
-
static afterDestroy<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>): This {
|
|
496
|
-
return this.setCallback('destroy', 'after', fn);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Run `fn` after the outermost transaction surrounding this record's
|
|
501
|
-
* save / destroy commits. When the record is touched outside a
|
|
502
|
-
* transaction, the callback fires immediately after the save.
|
|
503
|
-
*/
|
|
504
|
-
static afterCommit<This extends typeof Model>(
|
|
505
|
-
this: This,
|
|
506
|
-
fn: CallbackFn<InstanceType<This>>,
|
|
507
|
-
options?: { on?: 'create' | 'update' | 'destroy' | Array<'create' | 'update' | 'destroy'> },
|
|
508
|
-
): This {
|
|
509
|
-
return this.setCallback('commit', 'after', fn, options as never);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/** Run `fn` when the surrounding transaction rolls back. */
|
|
513
|
-
static afterRollback<This extends typeof Model>(
|
|
514
|
-
this: This,
|
|
515
|
-
fn: CallbackFn<InstanceType<This>>,
|
|
516
|
-
options?: { on?: 'create' | 'update' | 'destroy' | Array<'create' | 'update' | 'destroy'> },
|
|
517
|
-
): This {
|
|
518
|
-
return this.setCallback('rollback', 'after', fn, options as never);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/** Run `fn` whenever a new instance is constructed. */
|
|
522
|
-
static afterInitialize<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>): This {
|
|
523
|
-
return this.setCallback('initialize', 'after', fn);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/** Run `fn` when a record is hydrated from the database. */
|
|
527
|
-
static afterFind<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>): This {
|
|
528
|
-
return this.setCallback('find', 'after', fn);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/** Run `fn` after `touch()`. */
|
|
532
|
-
static afterTouch<This extends typeof Model>(this: This, fn: CallbackFn<InstanceType<This>>): This {
|
|
533
|
-
return this.setCallback('touch', 'after', fn);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/** Run a registered callback chain — typically used by ActiveRecord persistence. */
|
|
537
|
-
static async runCallbacks<This extends typeof Model>(
|
|
538
|
-
this: This,
|
|
539
|
-
event: CallbackEvent,
|
|
540
|
-
record: InstanceType<This>,
|
|
541
|
-
body: () => Promise<void | boolean>,
|
|
542
|
-
context?: string,
|
|
543
|
-
): Promise<boolean> {
|
|
544
|
-
return getRegistry(this).callbacks.run(event, record, body, context);
|
|
545
|
-
}
|
|
546
|
-
}
|
package/src/Name.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `Name` — Rails-style `ActiveModel::Name`. Exposes singular/plural/
|
|
3
|
-
* element/collection/route_key/param_key/i18n_key/human/uncountable
|
|
4
|
-
* derivations for a class name. We compute these purely from the class
|
|
5
|
-
* name via the inflector — no i18n, no class introspection.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { pluralize, underscore } from './inflector';
|
|
9
|
-
|
|
10
|
-
const UNCOUNTABLE = new Set(['equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep']);
|
|
11
|
-
|
|
12
|
-
const humanize = (word: string): string => {
|
|
13
|
-
const spaced = word.replace(/_/g, ' ').toLowerCase();
|
|
14
|
-
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export class Name {
|
|
18
|
-
/** The class itself (or its name, when called with a class-name string). */
|
|
19
|
-
readonly klass: { name: string };
|
|
20
|
-
/** The fully-qualified class name as written. */
|
|
21
|
-
readonly name: string;
|
|
22
|
-
/** Snake-case version of the (possibly namespaced) class name. e.g. `Post::TrackBack` -> `post_track_back`. */
|
|
23
|
-
readonly singular: string;
|
|
24
|
-
/** Plural form of `singular`. e.g. `post_track_back` -> `post_track_backs`. */
|
|
25
|
-
readonly plural: string;
|
|
26
|
-
/** Last segment, snake-cased. e.g. `Post::TrackBack` -> `track_back`. */
|
|
27
|
-
readonly element: string;
|
|
28
|
-
/** Forward-slash-joined collection name. e.g. `Post::TrackBack` -> `post/track_backs`. */
|
|
29
|
-
readonly collection: string;
|
|
30
|
-
/** Rails routing helper — plural element with optional namespace prefix. */
|
|
31
|
-
readonly route_key: string;
|
|
32
|
-
/** `singular` minus internal namespace separators. */
|
|
33
|
-
readonly param_key: string;
|
|
34
|
-
/** Slash-joined namespace key. e.g. `Post::TrackBack` -> `post/track_back`. */
|
|
35
|
-
readonly i18n_key: string;
|
|
36
|
-
/** Human-friendly element name. e.g. `track_back` -> `Track back`. */
|
|
37
|
-
readonly human: string;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Build a `Name` from a class (uses `klass.name`) or directly from a
|
|
41
|
-
* Ruby-style namespaced string ("Post::TrackBack").
|
|
42
|
-
*/
|
|
43
|
-
constructor(klassOrName: string | { name: string }) {
|
|
44
|
-
this.klass = typeof klassOrName === 'string' ? { name: klassOrName } : klassOrName;
|
|
45
|
-
this.name = this.klass.name;
|
|
46
|
-
const parts = this.name.split('::');
|
|
47
|
-
const last = parts[parts.length - 1]!;
|
|
48
|
-
const nsParts = parts.slice(0, -1).map((p) => underscore(p));
|
|
49
|
-
const lastSnake = underscore(last);
|
|
50
|
-
this.singular = [...nsParts, lastSnake].join('_');
|
|
51
|
-
this.plural = pluralize(this.singular);
|
|
52
|
-
this.element = lastSnake;
|
|
53
|
-
const elementPlural = pluralize(lastSnake);
|
|
54
|
-
this.collection = nsParts.length === 0 ? elementPlural : `${nsParts.join('/')}/${elementPlural}`;
|
|
55
|
-
this.route_key = nsParts.length === 0 ? elementPlural : `${nsParts.join('_')}_${elementPlural}`;
|
|
56
|
-
this.param_key = this.singular;
|
|
57
|
-
this.i18n_key = nsParts.length === 0 ? lastSnake : `${nsParts.join('/')}/${lastSnake}`;
|
|
58
|
-
this.human = humanize(lastSnake);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Whether the singular form is uncountable (plural === singular). */
|
|
62
|
-
get uncountable(): boolean {
|
|
63
|
-
return UNCOUNTABLE.has(this.singular.toLowerCase());
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
toString(): string {
|
|
67
|
-
return this.name;
|
|
68
|
-
}
|
|
69
|
-
}
|