@jwerre/vellum 1.0.1 → 1.1.1

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vellum
2
2
 
3
- Vellum is a lightweight, structural state management library for Svelte 5. Vellum provides a robust Model and Collection base powered by Svelte Runes.It bridges the gap between raw objects and complex state logic, offering a typed, class-based approach to managing data-heavy applications.
3
+ Vellum is a lightweight, structural state management library for Svelte 5. Vellum provides a robust Model and Collection base powered by Svelte Runes. It bridges the gap between raw objects and complex state logic, offering a typed, class-based approach to managing data-heavy applications.
4
4
 
5
5
  ## Features
6
6
 
@@ -27,7 +27,7 @@ Modern Svelte development often moves away from stores and toward raw $state obj
27
27
  npm install @jwerre/vellum
28
28
  ```
29
29
 
30
- ## Quick Start
30
+ ## Quick start
31
31
 
32
32
  ### Configuration
33
33
 
@@ -50,30 +50,52 @@ Extend the Model class to define your data structure and any derived state or bu
50
50
 
51
51
  ```ts
52
52
  import { Model } from '@jwerre/vellum';
53
- interface UserSchema {
53
+ interface BookSchema {
54
54
  id: number;
55
- firstName: string;
56
- lastName: string;
57
- role: 'admin' | 'user';
55
+ author: string;
56
+ genre: 'literature', 'romance' | 'adventure' | 'fantasy' | 'sci-fi' | 'mystery';
57
+ summary: string;
58
+ title: string;
59
+ year: number;
58
60
  }
59
61
 
60
- export class UserModel extends Model<UserSchema> {
62
+ export class Book extends Model<BookSchema> {
63
+
61
64
  endpoint() {
62
- return `/v1/user`;
65
+ return `/v1/books`;
66
+ }
67
+
68
+ defaults() {
69
+ return {
70
+ title: '',
71
+ author: '',
72
+ genre: 'literature',
73
+ summary: '',
74
+ year: new Date().getFullYear()
75
+ };
63
76
  }
64
77
 
65
- // Computed property using Svelte $derived
66
- fullName = $derived(`${this.get('firstName')} ${this.get('lastName')}`);
78
+ authorFirstName = $derived(`${this.get('author')?.split(' ')[0]}`);
67
79
 
68
- isAdmin() {
69
- return this.get('role') === 'admin';
80
+ authorLastName = $derived(`${this.get('author')?.split(' ')[1]}`);
81
+
82
+ isClassic() {
83
+ return this.get('year') < 1900;
70
84
  }
71
85
  }
72
86
 
73
- const user = new UserModel({ firstName: 'John', lastName: 'Doe', role: 'user' });
74
- await user.sync();
75
- console.log(user.id); // 1
76
- console.log(user.fullName); // John Doe
87
+ const book = new Book({
88
+ title: 'Crime and Punishment',
89
+ author: 'Fyodor Dostoevsky',
90
+ year: 1866,
91
+ genre: 'literature',
92
+ summary: 'An exploration of the mental anguish and moral dilemmas...',
93
+ });
94
+
95
+ await book.sync(); // issues POST request to endpoint
96
+ console.log(book.id); // unique ID created by POST request
97
+ console.log(book.authorFirstName); // Fyodor
98
+ console.log(book.isClassic()); // true
77
99
  ```
78
100
 
79
101
  ### Define a Collection
@@ -82,292 +104,816 @@ Manage groups of models with built-in reactivity.
82
104
 
83
105
  ```ts
84
106
  import { Collection } from '@jwerre/vellum';
85
- import { UserModel } from './UserModel.svelte.js';
107
+ import { Book } from './Book.svelte.js';
86
108
 
87
- export class UserCollection extends Collection<UserModel, UserSchema> {
88
- model = UserModel;
109
+ export class Books extends Collection<Book, BookSchema> {
110
+ model = Book;
89
111
 
90
112
  endpoint() {
91
- return `/v1/users`;
113
+ return `/v1/books`;
92
114
  }
93
115
 
94
116
  // Derived state for the entire collection
95
- adminCount = $derived(this.items.filter((u) => u.isAdmin()).length);
117
+ classicCount = $derived(this.items.filter((u) => u.isClassic()).length);
96
118
  }
97
119
  ```
98
120
 
99
- ### Use in Svelte Components
121
+ ### Use in Svelte components
100
122
 
101
123
  Vellum works seamlessly with Svelte 5 components.
102
124
 
103
125
  ```svelte
104
126
  <script lang="ts">
105
- import { UserCollection } from './UserCollection';
106
-
107
- const users = new UserCollection([{ id: 1, firstName: 'Jane', lastName: 'Doe', role: 'admin' }]);
108
-
109
- function addUser() {
110
- users.add({ id: 2, firstName: 'John', lastName: 'Smith', role: 'user' });
127
+ import { Books } from './Books';
128
+
129
+ const books = new Books([
130
+ {
131
+ id: 1,
132
+ title: 'The Great Gatsby',
133
+ author: 'F. Scott Fitzgerald',
134
+ year: 1925,
135
+ genre: 'literature'
136
+ },
137
+ {
138
+ id: 2,
139
+ title: 'To Kill a Mockingbird',
140
+ author: 'Harper Lee',
141
+ year: 1960,
142
+ genre: 'literature'
143
+ },
144
+ {
145
+ id: 3,
146
+ title: 'The Pillars of the Earth',
147
+ author: 'Ken Follett',
148
+ year: 1989,
149
+ genre: 'literature'
150
+ }
151
+ ]);
152
+
153
+ function addBook() {
154
+ books.add({
155
+ title: 'Moby Dick',
156
+ author: 'Herman Melville',
157
+ year: 1851,
158
+ genre: 'Adventure',
159
+ summary: 'The obsessive quest of Captain Ahab to seek revenge on the white whale.'
160
+ });
111
161
  }
112
162
  </script>
113
163
 
114
- <h1>Admins: {users.adminCount}</h1>
164
+ <h1>Classic Books: {books.classicCount}</h1>
115
165
 
116
166
  <ul>
117
- {#each users.items as user}
118
- {#if user.isAdmin()}
119
- <li>{user.fullName} ({user.get('email')})</li>
167
+ {#each books.items as book}
168
+ {#if book.isClassic()}
169
+ <li>{book.get('title')} ({book.get('year')}) - ({book.get('author')})</li>
120
170
  {/if}
121
171
  {/each}
122
172
  </ul>
123
173
 
124
- <button onclick={addUser}>Add User</button>
174
+ <button onclick={addBook}>Add Moby Dick</button>
125
175
  ```
126
176
 
127
- ## API Reference
177
+ ### Working example
178
+
179
+ There is a working example of Vellum in the `routes` directory. To run it, clone the repository, install dependencies, and run the development server:
180
+
181
+ git clone https://github.com/jwerre/vellum.git
182
+ cd vellum
183
+ npm install
184
+ npm run dev
185
+
186
+ ## API Documentation
187
+
188
+ <!-- Generated by documentation.js. Update this documentation by updating the source code. -->
189
+
190
+ #### Table of Contents
191
+
192
+ - [vellumConfig](#vellumconfig)
193
+ - [configureVellum](#configurevellum)
194
+ - [Parameters](#parameters)
195
+ - [Examples](#examples)
196
+ - [Model](#model)
197
+ - [Parameters](#parameters-1)
198
+ - [Examples](#examples-1)
199
+ - [idAttribute](#idattribute)
200
+ - [Examples](#examples-2)
201
+ - [validationError](#validationerror)
202
+ - [defaults](#defaults)
203
+ - [Examples](#examples-3)
204
+ - [get](#get)
205
+ - [Parameters](#parameters-2)
206
+ - [Examples](#examples-4)
207
+ - [has](#has)
208
+ - [Parameters](#parameters-3)
209
+ - [Examples](#examples-5)
210
+ - [unset](#unset)
211
+ - [Parameters](#parameters-4)
212
+ - [Examples](#examples-6)
213
+ - [clear](#clear)
214
+ - [Examples](#examples-7)
215
+ - [escape](#escape)
216
+ - [Parameters](#parameters-5)
217
+ - [Examples](#examples-8)
218
+ - [isNew](#isnew)
219
+ - [isValid](#isvalid)
220
+ - [Parameters](#parameters-6)
221
+ - [Examples](#examples-9)
222
+ - [validate](#validate)
223
+ - [Parameters](#parameters-7)
224
+ - [Examples](#examples-10)
225
+ - [sync](#sync)
226
+ - [Parameters](#parameters-8)
227
+ - [Examples](#examples-11)
228
+ - [fetch](#fetch)
229
+ - [Examples](#examples-12)
230
+ - [save](#save)
231
+ - [Parameters](#parameters-9)
232
+ - [Examples](#examples-13)
233
+ - [destroy](#destroy)
234
+ - [Examples](#examples-14)
235
+ - [toJSON](#tojson)
236
+ - [Examples](#examples-15)
237
+ - [validationError](#validationerror-1)
238
+ - [Collection](#collection)
239
+ - [Parameters](#parameters-10)
240
+ - [Examples](#examples-16)
241
+ - [items](#items)
242
+ - [length](#length)
243
+ - [add](#add)
244
+ - [Parameters](#parameters-11)
245
+ - [Examples](#examples-17)
246
+ - [reset](#reset)
247
+ - [Parameters](#parameters-12)
248
+ - [Examples](#examples-18)
249
+ - [find](#find)
250
+ - [Parameters](#parameters-13)
251
+ - [Examples](#examples-19)
252
+ - [fetch](#fetch-1)
253
+ - [Parameters](#parameters-14)
254
+ - [Examples](#examples-20)
255
+
256
+ ### vellumConfig
257
+
258
+ Global reactive state for Vellum configuration. Uses Svelte's $state rune to
259
+ create a reactive configuration object that automatically triggers updates when modified.
260
+
261
+ ### configureVellum
262
+
263
+ Helper function to update global Vellum configuration
264
+
265
+ Allows partial updates to the global configuration state. Only provided
266
+ properties will be updated, leaving others unchanged. Headers are merged
267
+ with existing headers rather than replaced entirely.
268
+
269
+ #### Parameters
270
+
271
+ - `config` **Partial\<VellumConfig>** Partial configuration object with properties to update
272
+ - `config.origin` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** New origin URL to set
273
+ - `config.headers` **Record<[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String), [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)>?** Headers to merge with existing headers
274
+ - `config.idAttribute` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** The default unique identifier attribute for models (optional, default `"id"`)
275
+
276
+ #### Examples
277
+
278
+ ```javascript
279
+ // Set the API origin
280
+ configureVellum({ origin: 'https://api.vellum.ai' });
281
+
282
+ // Add custom headers
283
+ configureVellum({
284
+ headers: {
285
+ Authorization: 'Bearer token123',
286
+ 'X-Custom-Header': 'value'
287
+ }
288
+ });
289
+
290
+ // Update both origin and headers
291
+ configureVellum({
292
+ origin: 'https://api.vellum.ai',
293
+ headers: { Authorization: 'Bearer token123' }
294
+ });
295
+ ```
296
+
297
+ ### Model
298
+
299
+ Abstract base class for creating model instances that interact with RESTful APIs.
128
300
 
129
- ### `Model<T>`
301
+ The Model class provides a structured way to manage data objects with full CRUD
302
+ (Create, Read, Update, Delete) capabilities. It includes built-in HTTP synchronization,
303
+ attribute management, and data validation features. This class is designed to work
304
+ with Svelte's reactivity system using the `$state` rune for automatic UI updates.
130
305
 
131
- The `Model` class provides a base class for creating data models with built-in CRUD operations and server synchronization.
306
+ Key features:
132
307
 
133
- #### Constructor
308
+ - Type-safe attribute access and manipulation
309
+ - Automatic HTTP synchronization with RESTful APIs
310
+ - Built-in HTML escaping for XSS prevention
311
+ - Configurable ID attributes for different database schemas
312
+ - Reactive attributes that integrate with Svelte's state management
313
+ - Support for both single attribute and bulk attribute operations
314
+
315
+ #### Parameters
316
+
317
+ - `data` (optional, default `{}`)
318
+
319
+ #### Examples
134
320
 
135
321
  ```javascript
136
- new Model((data = {}));
322
+ // Define a User model
323
+ interface UserAttributes {
324
+ id?: number;
325
+ name: string;
326
+ email: string;
327
+ createdAt?: Date;
328
+ }
329
+
330
+ class User extends Model<UserAttributes> {
331
+ endpoint() {
332
+ return '/users';
333
+ }
334
+ defaults() {
335
+ return { name: '', createdAt: new Date() };
336
+ }
337
+ }
338
+
339
+ // Create and use a model instance
340
+ const user = new User({ name: 'John Doe', email: 'john@example.com' });
341
+ await user.save(); // Creates new user on server
342
+ user.set('name', 'Jane Doe');
343
+ await user.save(); // Updates existing user
344
+ await user.destroy(); // Deletes user
137
345
  ```
138
346
 
139
- Creates a new Model instance with optional initial attributes.
347
+ #### idAttribute
140
348
 
141
- **Parameters:**
349
+ The name of the attribute that serves as the unique identifier for this model instance.
142
350
 
143
- - `data` (Object) - Optional partial object of attributes to initialize the model
351
+ This private field stores the attribute name that will be used to identify the model's
352
+ primary key when performing operations like determining if the model is new, constructing
353
+ URLs for API requests, and managing model identity. The default value is 'id', but it
354
+ can be customized through the ModelOptions parameter in the constructor.
144
355
 
145
- #### Abstract Properties
356
+ Type: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)
146
357
 
147
- Must be implemented by subclasses:
358
+ ##### Examples
148
359
 
149
- - `endpoint()` - Function that returns the base URL path for API endpoints (e.g., '/users')
360
+ ```javascript
361
+ // Default behavior uses 'id' as the identifier
362
+ const user = new User({ id: 1, name: 'John' });
150
363
 
151
- #### Methods
364
+ // Custom ID attribute can be specified in constructor options
365
+ class User extends Model<UserSchema> {
366
+ idAttribute = '_id
367
+ endpoint(): string {
368
+ return '/users';
369
+ }
370
+ }
371
+ const user = new User({ _id: '507f1f77bcf86cd799439011', name: 'John' });
372
+ ```
373
+
374
+ #### validationError
375
+
376
+ Gets the latest validation error.
377
+
378
+ #### defaults
152
379
 
153
- ##### get(key)
380
+ Provides default attribute values for new model instances.
381
+
382
+ This method is called during model construction to establish initial attribute
383
+ values before applying any user-provided data. Subclasses can override this
384
+ method to define default values for their specific attributes, ensuring that
385
+ models always have sensible initial state.
386
+
387
+ The defaults are applied first, then any data passed to the constructor will
388
+ override these default values. This allows for flexible model initialization
389
+ where some attributes have fallback values while others can be explicitly set.
390
+
391
+ ##### Examples
392
+
393
+ ```javascript
394
+ // Override in a User model subclass
395
+ protected defaults(): Partial<UserAttributes> {
396
+ return {
397
+ role: 'user',
398
+ isActive: true,
399
+ createdAt: new Date()
400
+ };
401
+ }
402
+ ```
403
+
404
+ ```javascript
405
+ // Creating a model with defaults
406
+ const user = new User({ name: 'John' });
407
+ // Resulting attributes: { role: 'user', isActive: true, createdAt: Date, name: 'John' }
408
+ ```
409
+
410
+ Returns **Partial\<T>** A partial object containing default attribute values
411
+
412
+ #### get
154
413
 
155
414
  Retrieves the value of a specific attribute from the model.
156
415
 
157
- **Parameters:**
416
+ This method provides type-safe access to model attributes, ensuring that the
417
+ returned value matches the expected type for the given key. It acts as a
418
+ getter for the internal attributes stored in the model instance.
419
+
420
+ ##### Parameters
158
421
 
159
- - `key` - The attribute key to retrieve
422
+ - `key` **K** The attribute key to retrieve the value for
160
423
 
161
- **Returns:** The value associated with the specified key
424
+ ##### Examples
162
425
 
163
426
  ```javascript
427
+ // Assuming a User model with attributes { id: number, name: string }
164
428
  const user = new User({ id: 1, name: 'John Doe' });
165
- const name = user.get('name'); // Returns 'John Doe'
429
+ const name = user.get('name'); // Returns 'John Doe' (string)
430
+ const id = user.get('id'); // Returns 1 (number)
166
431
  ```
167
432
 
168
- ##### set(attrs)
433
+ #### has
434
+
435
+ Checks whether a specific attribute has a non-null, non-undefined value.
169
436
 
170
- Updates multiple attributes on the model instance.
437
+ This method provides a way to determine if an attribute exists and has a
438
+ meaningful value. It returns true if the attribute is set to any value
439
+ other than null or undefined, including falsy values like false, 0, or
440
+ empty strings, which are considered valid values.
171
441
 
172
- **Parameters:**
442
+ ##### Parameters
173
443
 
174
- - `attrs` - Partial object containing attributes to update
444
+ - `key` **K** The attribute key to check
445
+
446
+ ##### Examples
175
447
 
176
448
  ```javascript
449
+ // Assuming a User model with attributes { id: number, name: string, email?: string }
450
+ const user = new User({ id: 1, name: 'John', email: null });
451
+
452
+ user.has('id'); // Returns true (value is 1)
453
+ user.has('name'); // Returns true (value is 'John')
454
+ user.has('email'); // Returns false (value is null)
455
+
456
+ // Even falsy values are considered "set"
457
+ user.set({ name: '' });
458
+ user.has('name'); // Returns true (empty string is a valid value)
459
+ ```
460
+
461
+ Returns **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** True if the attribute has a non-null, non-undefined value
462
+
463
+ #### unset
464
+
465
+ Removes a specific attribute from the model by deleting it from the internal attributes hash.
466
+
467
+ This method permanently removes an attribute from the model instance. Once unset,
468
+ the attribute will no longer exist on the model and subsequent calls to get() for
469
+ that key will return undefined. This is different from setting an attribute to
470
+ null or undefined, as the property is completely removed from the attributes object.
471
+
472
+ ##### Parameters
473
+
474
+ - `key` **K** The attribute key to remove from the model
475
+
476
+ ##### Examples
477
+
478
+ ```javascript
479
+ // Assuming a User model with attributes { id: number, name: string, email: string }
480
+ const user = new User({ id: 1, name: 'John', email: 'john@example.com' });
481
+
482
+ user.has('email'); // Returns true
483
+ user.unset('email'); // Remove the email attribute
484
+ user.has('email'); // Returns false
485
+ user.get('email'); // Returns undefined
486
+
487
+ // The attribute is completely removed, not just set to undefined
488
+ const userData = user.toJSON(); // { id: 1, name: 'John' }
489
+ ```
490
+
491
+ Returns **void**&#x20;
492
+
493
+ #### clear
494
+
495
+ Removes all attributes from the model, including the id attribute.
496
+
497
+ This method completely clears the model instance by removing all stored attributes,
498
+ effectively resetting it to an empty state. After calling clear(), the model will
499
+ behave as if it were newly instantiated with no data. This includes removing the
500
+ ID attribute, which means the model will be considered "new" after clearing.
501
+
502
+ This is useful when you want to reuse a model instance for different data or
503
+ reset a model to its initial state without creating a new instance.
504
+
505
+ ##### Examples
506
+
507
+ ```javascript
508
+ // Clear all data from a user model
509
+ const user = new User({ id: 1, name: 'John', email: 'john@example.com' });
510
+ user.clear();
511
+
512
+ user.has('id'); // Returns false
513
+ user.has('name'); // Returns false
514
+ user.isNew(); // Returns true
515
+ user.toJSON(); // Returns {}
516
+
517
+ // Model can be reused with new data
177
518
  user.set({ name: 'Jane', email: 'jane@example.com' });
178
519
  ```
179
520
 
180
- ##### isNew()
521
+ Returns **void**&#x20;
522
+
523
+ #### escape
524
+
525
+ Retrieves and escapes the HTML content of a specific attribute from the model.
526
+
527
+ This method provides a safe way to access model attributes that may contain
528
+ user-generated content or data that will be rendered in HTML contexts. It
529
+ automatically applies HTML escaping to prevent XSS attacks and ensure safe
530
+ rendering of potentially dangerous content.
531
+
532
+ The method uses the escapeHTML utility function to convert special HTML
533
+ characters (such as <, >, &, ", and ') into their corresponding HTML entities,
534
+ making the content safe for direct insertion into HTML templates.
535
+
536
+ ##### Parameters
537
+
538
+ - `key` **K** The attribute key to retrieve and escape the value for
539
+
540
+ ##### Examples
541
+
542
+ ```javascript
543
+ // Assuming a Post model with attributes { id: number, title: string, content: string }
544
+ const post = new Post({
545
+ id: 1,
546
+ title: 'Hello <script>alert("XSS")</script>',
547
+ content: 'This is "safe" & secure content'
548
+ });
549
+
550
+ const safeTitle = post.escape('title');
551
+ // Returns: 'Hello &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;'
552
+
553
+ const safeContent = post.escape('content');
554
+ // Returns: 'This is &quot;safe&quot; &amp; secure content'
555
+ ```
556
+
557
+ #### isNew
181
558
 
182
559
  Determines whether this model instance is new (not yet persisted).
560
+ A model is considered new if it doesn't have an 'id' or '\_id' attribute.
561
+
562
+ Returns **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** true if the model is new, false if it has been persisted
563
+
564
+ #### isValid
565
+
566
+ Validates the current model instance and returns whether it passes validation.
567
+
568
+ This method performs validation on the model's current attributes using the
569
+ validate() method defined by the subclass. It's a convenience method that
570
+ allows you to check if a model is valid without having to manually call
571
+ the validation logic.
572
+
573
+ If validation fails, the validationError property will be set with details
574
+ about what went wrong. If validation passes, validationError will be cleared.
575
+
576
+ ##### Parameters
577
+
578
+ - `options` **ValidationOptions?** Optional validation configuration
579
+ - `options.silent` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)?** If true, suppresses validation error setting
580
+
581
+ ##### Examples
582
+
583
+ ```javascript
584
+ // Check if a user model is valid
585
+ const user = new User({ name: '', email: 'invalid-email' });
586
+
587
+ if (!user.isValid()) {
588
+ console.log('Validation failed:', user.validationError);
589
+ // Handle validation errors
590
+ } else {
591
+ // Proceed with saving or other operations
592
+ await user.save();
593
+ }
594
+ ```
595
+
596
+ ```javascript
597
+ // Validate silently without setting validationError
598
+ const isValid = user.isValid({ silent: true });
599
+ // user.validationError remains unchanged
600
+ ```
601
+
602
+ Returns **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** True if the model passes validation, false otherwise
603
+
604
+ #### validate
605
+
606
+ Validates the model's attributes using custom validation logic.
607
+
608
+ This method is intended to be overridden by subclasses to implement custom
609
+ validation rules. By default, it returns undefined (no validation errors).
610
+ If validation fails, this method should return an error - either a simple
611
+ string message or a complete error object.
183
612
 
184
- **Returns:** `true` if the model has no ID, `false` otherwise
613
+ The validate method is automatically called by save() before persisting data,
614
+ and can also be explicitly called by set() when the {validate: true} option
615
+ is passed. If validation fails, the save operation is aborted and model
616
+ attributes are not modified.
617
+
618
+ ##### Parameters
619
+
620
+ - `attributes` **Partial\<T>** The attributes to validate
621
+ - `options` **any?** Additional options passed from set() or save()
622
+
623
+ ##### Examples
185
624
 
186
625
  ```javascript
187
- const newUser = new User({ name: 'John' });
188
- console.log(newUser.isNew()); // true
626
+ // Override in a User model subclass
627
+ validate(attributes: Partial<UserAttributes>) {
628
+ if (!attributes.email) {
629
+ return 'Email is required';
630
+ }
631
+ if (!attributes.email.includes('@')) {
632
+ return { email: 'Invalid email format' };
633
+ }
634
+ // Return undefined if validation passes
635
+ }
189
636
  ```
190
637
 
191
- ##### sync(method, body, options)
638
+ Returns **any** Returns undefined if valid, or an error (string/object) if invalid
639
+
640
+ #### sync
192
641
 
193
642
  Performs HTTP synchronization with the server for CRUD operations.
194
643
 
195
- **Parameters:**
644
+ This method handles all HTTP communication between the model and the server,
645
+ automatically constructing the appropriate URL based on the model's ID and
646
+ endpoint(). It supports all standard REST operations and provides type-safe
647
+ response handling.
196
648
 
197
- - `method` - HTTP method ('GET', 'POST', 'PUT', 'PATCH', 'DELETE'), defaults to 'GET'
198
- - `body` - Optional request body data
199
- - `options` - Optional configuration overrides
649
+ The URL construction follows REST conventions:
200
650
 
201
- **Returns:** Promise resolving to server response data or null
651
+ - For new models (no ID): uses collection endpoint `${baseUrl}${endpoint()}`
652
+ - For existing models (with ID): uses resource endpoint `${baseUrl}${endpoint()}/${id}`
653
+
654
+ ##### Parameters
655
+
656
+ - `method` **(`"GET"` | `"POST"` | `"PUT"` | `"PATCH"` | `"DELETE"`)** The HTTP method to use (defaults to 'GET') (optional, default `'GET'`)
657
+ - `body` **(Record<[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String), unknown> | T)?** Optional request body data to send
658
+ - `options` (optional, default `{}`)
659
+
660
+ ##### Examples
202
661
 
203
662
  ```javascript
204
- // Fetch user data
663
+ // Fetch a user by ID (default 'GET' request)
205
664
  const userData = await user.sync();
206
665
 
207
- // Create new user
208
- const newUser = await user.sync('POST', { name: 'John' });
666
+ // Create a new user (POST request)
667
+ const newUser = await user.sync('POST', { name: 'John', email: 'john@example.com' });
209
668
 
210
- // Update user
211
- const updated = await user.sync('PUT', user.toJSON());
669
+ // Update an existing user (PUT request)
670
+ const updatedUser = await user.sync('PUT', user.toJSON());
671
+
672
+ // Delete a user (DELETE request)
673
+ await user.sync('DELETE'); // Returns null for 204 responses
212
674
  ```
213
675
 
214
- ##### fetch()
676
+ - Throws **[Error](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error)** Throws an error if the HTTP response is not successful
677
+
678
+ Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<(R | null)>** The server response data, or null for 204 No Content responses
679
+
680
+ #### fetch
215
681
 
216
682
  Fetches data from the server and updates the model's attributes.
217
683
 
218
- **Returns:** Promise
684
+ This method performs a GET request to retrieve the latest data for this model
685
+ instance from the server. If the model has an ID, it will fetch the specific
686
+ resource; if it's a new model without an ID, it will make a request to the
687
+ collection endpoint.
688
+
689
+ Upon successful retrieval, the model's attributes are automatically updated
690
+ with the server response data. This method is useful for refreshing a model's
691
+ state or loading data after creating a model instance with just an ID.
692
+
693
+ ##### Examples
219
694
 
220
695
  ```javascript
696
+ // Fetch data for an existing user
221
697
  const user = new User({ id: 1 });
222
- await user.fetch(); // Model now contains full user data
698
+ await user.fetch(); // Model now contains full user data from server
699
+
700
+ // Refresh a model's data
701
+ await existingUser.fetch(); // Updates with latest server data
223
702
  ```
224
703
 
225
- ##### save()
704
+ - Throws **[Error](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error)** Throws an error if the HTTP request fails or server returns an error
226
705
 
227
- Saves the model by creating (POST) or updating (PUT) the server resource.
706
+ Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)\<void>** A promise that resolves when the fetch operation completes
228
707
 
229
- **Returns:** Promise
708
+ #### save
230
709
 
231
- ```javascript
232
- // Create new user
233
- const newUser = new User({ name: 'John' });
234
- await newUser.save(); // POST request
710
+ Saves the model to the server by creating a new resource or updating an existing one.
235
711
 
236
- // Update existing user
237
- user.set({ name: 'Jane' });
238
- await user.save(); // PUT request
239
- ```
712
+ This method automatically determines whether to create or update based on the model's
713
+ state. If the model is new (has no ID), it performs a POST request to create a new
714
+ resource. If the model already exists (has an ID), it performs a PUT request to
715
+ update the existing resource.
240
716
 
241
- ##### destroy()
717
+ After a successful save operation, the model's attributes are updated with any
718
+ data returned from the server. This is particularly useful when the server
719
+ generates additional fields (like timestamps, computed values, or normalized data)
720
+ during the save process.
242
721
 
243
- Deletes the model from the server.
722
+ ##### Parameters
723
+
724
+ - `options` &#x20;
244
725
 
245
- **Returns:** Promise
726
+ ##### Examples
246
727
 
247
728
  ```javascript
248
- await user.destroy(); // DELETE request to /users/1
729
+ // Create a new user
730
+ const newUser = new User({ name: 'John', email: 'john@example.com' });
731
+ await newUser.save(); // POST request, user now has ID from server
732
+
733
+ // Update an existing user
734
+ existingUser.set({ name: 'Jane' });
735
+ await existingUser.save(); // PUT request with updated data
736
+
737
+ // Save without validation
738
+ await user.save({ validate: false });
739
+
740
+ // Save with custom endpoint and headers
741
+ await user.save({
742
+ endpoint: '/api/v2/users',
743
+ headers: { 'Custom-Header': 'value' }
744
+ });
249
745
  ```
250
746
 
251
- ##### toJSON()
747
+ - Throws **[Error](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error)** Throws an error if the HTTP request fails or server returns an error
252
748
 
253
- Returns a plain object representation of the model's attributes.
749
+ Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)>** A promise that resolves to true if save succeeds, false if validation fails
254
750
 
255
- **Returns:** Plain object containing all attributes
751
+ #### destroy
752
+
753
+ Deletes the model from the server.
754
+
755
+ This method performs a DELETE request to remove the model's corresponding resource
756
+ from the server. The method only executes if the model has an ID (i.e., it exists
757
+ on the server). If the model is new and has no ID, the method will return without
758
+ performing any operation.
759
+
760
+ The DELETE request is sent to the model's specific resource endpoint using the
761
+ pattern `${baseUrl}${endpoint()}/${id}`. After successful deletion, the model
762
+ instance remains in memory but the corresponding server resource is removed.
763
+
764
+ ##### Examples
256
765
 
257
766
  ```javascript
767
+ // Delete an existing user
258
768
  const user = new User({ id: 1, name: 'John' });
259
- const userData = user.toJSON();
260
- // Returns: { id: 1, name: 'John' }
769
+ await user.destroy(); // DELETE request to /users/1
770
+
771
+ // Attempting to destroy a new model (no operation performed)
772
+ const newUser = new User({ name: 'Jane' }); // No ID
773
+ await newUser.destroy(); // Returns immediately, no HTTP request
261
774
  ```
262
775
 
263
- #### Example Usage
776
+ - Throws **[Error](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error)** Throws an error if the HTTP request fails or server returns an error
264
777
 
265
- ```javascript
266
- class User extends Model {
267
- endpoint() {
268
- return '/users';
269
- }
270
- }
778
+ Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)\<void>** A promise that resolves when the delete operation completes
271
779
 
272
- // Create and save new user
273
- const user = new User({ name: 'John', email: 'john@example.com' });
274
- await user.save();
780
+ #### toJSON
275
781
 
276
- // Fetch existing user
277
- const existingUser = new User({ id: 1 });
278
- await existingUser.fetch();
782
+ Returns a plain JavaScript object representation of the model's attributes.
279
783
 
280
- // Update user
281
- existingUser.set({ name: 'Jane' });
282
- await existingUser.save();
784
+ This method creates a shallow copy of the model's internal attributes, returning
785
+ them as a plain object. This is useful for serialization, debugging, or when you
786
+ need to pass the model's data to functions that expect plain objects rather than
787
+ model instances.
788
+
789
+ The returned object is a copy, so modifications to it will not affect the original
790
+ model's attributes. This method is commonly used internally by other model methods
791
+ (like save()) when preparing data for HTTP requests.
792
+
793
+ ##### Examples
794
+
795
+ ```javascript
796
+ // Get plain object representation
797
+ const user = new User({ id: 1, name: 'John', email: 'john@example.com' });
798
+ const userData = user.toJSON();
799
+ // Returns: { id: 1, name: 'John', email: 'john@example.com' }
283
800
 
284
- // Delete user
285
- await existingUser.destroy();
801
+ // Useful for serialization
802
+ const jsonString = JSON.stringify(user.toJSON());
286
803
  ```
287
804
 
288
- ### `Collection<M, T>`
805
+ Returns **T** A plain object containing all of the model's attributes
289
806
 
290
- The `Collection` class provides a reactive container for managing groups of Model instances with automatic UI updates.
807
+ ### validationError
291
808
 
292
- #### Constructor
809
+ Validation error property that gets set when validation fails.
810
+ This property contains the error returned by the validate method.
293
811
 
294
- ```javascript
295
- new Collection((models = []));
296
- ```
812
+ ### Collection
813
+
814
+ Abstract base class for managing collections of Model instances.
297
815
 
298
- Creates a new Collection instance with optional initial data.
816
+ Provides a reactive collection that can be populated with data, fetched from a server,
817
+ and manipulated with type-safe operations. The collection is backed by Svelte's reactivity
818
+ system for automatic UI updates.
299
819
 
300
- **Parameters:**
820
+ #### Parameters
301
821
 
302
- - `models` (Array) - Optional array of data objects to initialize the collection
822
+ - `models` Optional array of data objects to initialize the collection with (optional, default `[]`)
303
823
 
304
- #### Properties
824
+ #### Examples
305
825
 
306
- - `items` - Reactive array of model instances in the collection
307
- - `length` - Number of items in the collection (read-only)
826
+ ```javascript
827
+ class UserCollection extends Collection<UserModel, User> {
828
+ model = UserModel;
829
+ endpoint = () => '/api/users';
830
+ }
308
831
 
309
- #### Abstract Properties
832
+ const users = new UserCollection();
833
+ await users.fetch(); // Loads users from API
834
+ users.add({ name: 'John', email: 'john@example.com' }); // Adds new user
835
+ ```
310
836
 
311
- These must be implemented by subclasses:
837
+ #### items
312
838
 
313
- - `model` - The Model class constructor for creating instances
314
- - `endpoint()` - Function that returns the API endpoint URL
839
+ Reactive array of model instances in the collection
315
840
 
316
- #### Methods
841
+ #### length
317
842
 
318
- ##### add(data)
843
+ Gets the number of items in the collection
844
+
845
+ #### add
319
846
 
320
847
  Adds a new item to the collection.
321
848
 
322
- **Parameters:**
849
+ ##### Parameters
323
850
 
324
- - `data` - Raw data object or existing model instance
851
+ - `data` Either raw data of type T or an existing model instance of type M
325
852
 
326
- **Returns:** The model instance that was added
853
+ ##### Examples
327
854
 
328
855
  ```javascript
856
+ // Add raw data
329
857
  const user = collection.add({ name: 'John', email: 'john@example.com' });
858
+
859
+ // Add existing model instance
860
+ const existingUser = new UserModel({ name: 'Jane' });
861
+ collection.add(existingUser);
330
862
  ```
331
863
 
332
- ##### reset(data)
864
+ Returns **any** The model instance that was added to the collection
333
865
 
334
- Replaces all items in the collection with new data.
866
+ #### reset
335
867
 
336
- **Parameters:**
868
+ Resets the collection with new data, replacing all existing items.
337
869
 
338
- - `data` - Array of raw data objects
870
+ ##### Parameters
871
+
872
+ - `data` An array of raw data objects to populate the collection with
873
+
874
+ ##### Examples
339
875
 
340
876
  ```javascript
877
+ // Reset collection with new user data
341
878
  collection.reset([
342
- { id: 1, name: 'John' },
343
- { id: 2, name: 'Jane' }
879
+ { id: 1, name: 'John', email: 'john@example.com' },
880
+ { id: 2, name: 'Jane', email: 'jane@example.com' }
344
881
  ]);
345
882
  ```
346
883
 
347
- ##### find(query)
884
+ #### find
348
885
 
349
- Finds the first item matching the query object.
886
+ Finds the first item in the collection that matches the given query.
350
887
 
351
- **Parameters:**
888
+ ##### Parameters
352
889
 
353
- - `query` - Object with key-value pairs to match
890
+ - `query` An object containing key-value pairs to match against items in the collection.
891
+ Only items that match all specified properties will be returned.
354
892
 
355
- **Returns:** The first matching item or `undefined`
893
+ ##### Examples
356
894
 
357
895
  ```javascript
896
+ // Find a user by ID
358
897
  const user = collection.find({ id: 123 });
898
+
899
+ // Find by multiple properties
359
900
  const activeAdmin = collection.find({ role: 'admin', status: 'active' });
360
901
  ```
361
902
 
362
- ##### fetch(options)
903
+ Returns **any** The first matching item, or undefined if no match is found.
904
+
905
+ #### fetch
363
906
 
364
907
  Fetches data from the server and populates the collection.
365
908
 
366
- **Parameters:**
909
+ ##### Parameters
367
910
 
368
- - `options.search` - Optional search parameters for the query string
911
+ - `options` Configuration options for the fetch request (optional, default `{}`)
912
+ - `options.endpoint` Optional endpoint to use if different than this.endpoint()
913
+ - `options.search` Optional search parameters to include in the query string.
914
+ Keys and values will be converted to strings and URL-encoded.
369
915
 
370
- **Returns:** Promise
916
+ ##### Examples
371
917
 
372
918
  ```javascript
373
919
  // Fetch all items
@@ -378,3 +924,5 @@ await collection.fetch({
378
924
  search: { limit: 30, after: 29 }
379
925
  });
380
926
  ```
927
+
928
+ - Throws **[Error](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error)** Throws an error if the HTTP request fails or returns a non-ok status