@jwerre/vellum 1.1.0 → 1.2.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/README.md CHANGED
@@ -178,12 +178,10 @@ Vellum works seamlessly with Svelte 5 components.
178
178
 
179
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
180
 
181
- ```
182
- git clone https://github.com/jwerre/vellum.git
183
- cd vellum
184
- npm install
185
- npm run dev
186
- ```
181
+ git clone https://github.com/jwerre/vellum.git
182
+ cd vellum
183
+ npm install
184
+ npm run dev
187
185
 
188
186
  ## API Documentation
189
187
 
@@ -198,58 +196,63 @@ npm run dev
198
196
  - [Model](#model)
199
197
  - [Parameters](#parameters-1)
200
198
  - [Examples](#examples-1)
201
- - [validationError](#validationerror)
202
199
  - [idAttribute](#idattribute)
200
+ - [Examples](#examples-2)
201
+ - [validationError](#validationerror)
202
+ - [defaults](#defaults)
203
+ - [Examples](#examples-3)
203
204
  - [get](#get)
204
205
  - [Parameters](#parameters-2)
205
- - [Examples](#examples-2)
206
+ - [Examples](#examples-4)
206
207
  - [has](#has)
207
208
  - [Parameters](#parameters-3)
208
- - [Examples](#examples-3)
209
+ - [Examples](#examples-5)
209
210
  - [unset](#unset)
210
211
  - [Parameters](#parameters-4)
211
- - [Examples](#examples-4)
212
+ - [Examples](#examples-6)
212
213
  - [clear](#clear)
213
- - [Examples](#examples-5)
214
+ - [Examples](#examples-7)
214
215
  - [escape](#escape)
215
216
  - [Parameters](#parameters-5)
216
- - [Examples](#examples-6)
217
+ - [Examples](#examples-8)
217
218
  - [isNew](#isnew)
218
219
  - [isValid](#isvalid)
219
220
  - [Parameters](#parameters-6)
220
- - [Examples](#examples-7)
221
+ - [Examples](#examples-9)
221
222
  - [validate](#validate)
222
223
  - [Parameters](#parameters-7)
223
- - [Examples](#examples-8)
224
+ - [Examples](#examples-10)
224
225
  - [sync](#sync)
225
226
  - [Parameters](#parameters-8)
226
- - [Examples](#examples-9)
227
+ - [Examples](#examples-11)
227
228
  - [fetch](#fetch)
228
- - [Examples](#examples-10)
229
+ - [Examples](#examples-12)
229
230
  - [save](#save)
230
231
  - [Parameters](#parameters-9)
231
- - [Examples](#examples-11)
232
+ - [Examples](#examples-13)
232
233
  - [destroy](#destroy)
233
- - [Examples](#examples-12)
234
+ - [Examples](#examples-14)
234
235
  - [toJSON](#tojson)
235
- - [Examples](#examples-13)
236
+ - [Examples](#examples-15)
237
+ - [validationError](#validationerror-1)
236
238
  - [Collection](#collection)
237
239
  - [Parameters](#parameters-10)
238
- - [Examples](#examples-14)
240
+ - [Examples](#examples-16)
239
241
  - [items](#items)
240
242
  - [length](#length)
241
243
  - [add](#add)
242
244
  - [Parameters](#parameters-11)
243
- - [Examples](#examples-15)
245
+ - [Examples](#examples-17)
246
+ - [sort](#sort)
244
247
  - [reset](#reset)
245
248
  - [Parameters](#parameters-12)
246
- - [Examples](#examples-16)
249
+ - [Examples](#examples-18)
247
250
  - [find](#find)
248
251
  - [Parameters](#parameters-13)
249
- - [Examples](#examples-17)
252
+ - [Examples](#examples-19)
250
253
  - [fetch](#fetch-1)
251
254
  - [Parameters](#parameters-14)
252
- - [Examples](#examples-18)
255
+ - [Examples](#examples-20)
253
256
 
254
257
  ### vellumConfig
255
258
 
@@ -269,6 +272,7 @@ with existing headers rather than replaced entirely.
269
272
  - `config` **Partial\<VellumConfig>** Partial configuration object with properties to update
270
273
  - `config.origin` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** New origin URL to set
271
274
  - `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
275
+ - `config.idAttribute` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** The default unique identifier attribute for models (optional, default `"id"`)
272
276
 
273
277
  #### Examples
274
278
 
@@ -312,7 +316,6 @@ Key features:
312
316
  #### Parameters
313
317
 
314
318
  - `data` (optional, default `{}`)
315
- - `options` (optional, default `{}`)
316
319
 
317
320
  #### Examples
318
321
 
@@ -326,9 +329,12 @@ interface UserAttributes {
326
329
  }
327
330
 
328
331
  class User extends Model<UserAttributes> {
329
- endpoint(): string {
332
+ endpoint() {
330
333
  return '/users';
331
334
  }
335
+ defaults() {
336
+ return { name: '', createdAt: new Date() };
337
+ }
332
338
  }
333
339
 
334
340
  // Create and use a model instance
@@ -336,40 +342,73 @@ const user = new User({ name: 'John Doe', email: 'john@example.com' });
336
342
  await user.save(); // Creates new user on server
337
343
  user.set('name', 'Jane Doe');
338
344
  await user.save(); // Updates existing user
345
+ await user.destroy(); // Deletes user
339
346
  ```
340
347
 
341
- ```javascript
342
- // Using custom ID attribute (e.g., MongoDB _id)
343
- interface MongoUserAttributes {
344
- _id?: string;
345
- username: string;
346
- profile: {
347
- firstName: string;
348
- lastName: string;
349
- };
350
- }
348
+ #### idAttribute
351
349
 
352
- class MongoUser extends Model<MongoUserAttributes> {
353
- constructor(data?: Partial<MongoUserAttributes>) {
354
- super(data, { idAttribute: '_id' });
355
- }
350
+ The name of the attribute that serves as the unique identifier for this model instance.
356
351
 
357
- endpoint(): string {
358
- return '/api/users';
359
- }
352
+ This private field stores the attribute name that will be used to identify the model's
353
+ primary key when performing operations like determining if the model is new, constructing
354
+ URLs for API requests, and managing model identity. The default value is 'id', but it
355
+ can be customized through the ModelOptions parameter in the constructor.
356
+
357
+ Type: [string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)
358
+
359
+ ##### Examples
360
+
361
+ ```javascript
362
+ // Default behavior uses 'id' as the identifier
363
+ const user = new User({ id: 1, name: 'John' });
364
+
365
+ // Custom ID attribute can be specified in constructor options
366
+ class User extends Model<UserSchema> {
367
+ idAttribute = '_id
368
+ endpoint(): string {
369
+ return '/users';
370
+ }
360
371
  }
372
+ const user = new User({ _id: '507f1f77bcf86cd799439011', name: 'John' });
361
373
  ```
362
374
 
363
375
  #### validationError
364
376
 
365
- Validation error property that gets set when validation fails.
366
- This property contains the error returned by the validate method.
377
+ Gets the latest validation error.
367
378
 
368
- #### idAttribute
379
+ #### defaults
380
+
381
+ Provides default attribute values for new model instances.
369
382
 
370
- Gets the current ID attribute name used by this model instance.
383
+ This method is called during model construction to establish initial attribute
384
+ values before applying any user-provided data. Subclasses can override this
385
+ method to define default values for their specific attributes, ensuring that
386
+ models always have sensible initial state.
371
387
 
372
- Returns **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** The name of the attribute used as the ID field
388
+ The defaults are applied first, then any data passed to the constructor will
389
+ override these default values. This allows for flexible model initialization
390
+ where some attributes have fallback values while others can be explicitly set.
391
+
392
+ ##### Examples
393
+
394
+ ```javascript
395
+ // Override in a User model subclass
396
+ protected defaults(): Partial<UserAttributes> {
397
+ return {
398
+ role: 'user',
399
+ isActive: true,
400
+ createdAt: new Date()
401
+ };
402
+ }
403
+ ```
404
+
405
+ ```javascript
406
+ // Creating a model with defaults
407
+ const user = new User({ name: 'John' });
408
+ // Resulting attributes: { role: 'user', isActive: true, createdAt: Date, name: 'John' }
409
+ ```
410
+
411
+ Returns **Partial\<T>** A partial object containing default attribute values
373
412
 
374
413
  #### get
375
414
 
@@ -766,6 +805,11 @@ const jsonString = JSON.stringify(user.toJSON());
766
805
 
767
806
  Returns **T** A plain object containing all of the model's attributes
768
807
 
808
+ ### validationError
809
+
810
+ Validation error property that gets set when validation fails.
811
+ This property contains the error returned by the validate method.
812
+
769
813
  ### Collection
770
814
 
771
815
  Abstract base class for managing collections of Model instances.
@@ -820,6 +864,11 @@ collection.add(existingUser);
820
864
 
821
865
  Returns **any** The model instance that was added to the collection
822
866
 
867
+ #### sort
868
+
869
+ Sorts the collection using the comparator if one is defined.
870
+ Called automatically when items are added to the collection.
871
+
823
872
  #### reset
824
873
 
825
874
  Resets the collection with new data, replacing all existing items.
@@ -33,6 +33,46 @@ export declare abstract class Collection<M extends Model<T>, T extends object> {
33
33
  };
34
34
  /** Returns the API endpoint URL for this collection */
35
35
  abstract endpoint(): string;
36
+ /**
37
+ * Optional comparator for sorting the collection.
38
+ *
39
+ * By default, there is no comparator for a collection. If you define a comparator,
40
+ * it will be used to sort the collection any time a model is added.
41
+ *
42
+ * A comparator can be defined in three ways:
43
+ *
44
+ * 1. **sortBy** - Pass a function that takes a single model argument and returns
45
+ * a numeric or string value by which the model should be ordered relative to others.
46
+ *
47
+ * 2. **sort** - Pass a comparator function that takes two model arguments and returns
48
+ * -1 if the first model should come before the second, 0 if they are of the same
49
+ * rank, and 1 if the first model should come after.
50
+ *
51
+ * 3. **attribute name** - Pass a string indicating the attribute to sort by.
52
+ *
53
+ * Note: The implementation depends on the arity of your comparator function to
54
+ * determine between sortBy (1 argument) and sort (2 arguments) styles, so be
55
+ * careful if your comparator function is bound.
56
+ *
57
+ * @returns A comparator function, string attribute name, or undefined for no sorting
58
+ *
59
+ * @example
60
+ * // Sort by attribute name
61
+ * comparator = () => 'createdAt';
62
+ *
63
+ * @example
64
+ * // Sort using sortBy (single argument)
65
+ * comparator = () => (model: UserModel) => model.get('age');
66
+ *
67
+ * @example
68
+ * // Sort using sort comparator (two arguments)
69
+ * comparator = () => (a: UserModel, b: UserModel) => {
70
+ * if (a.get('priority') < b.get('priority')) return -1;
71
+ * if (a.get('priority') > b.get('priority')) return 1;
72
+ * return 0;
73
+ * };
74
+ */
75
+ comparator?(): string | ((model: M) => string | number) | ((a: M, b: M) => number) | undefined;
36
76
  /**
37
77
  * Creates a new Collection instance.
38
78
  *
@@ -66,6 +106,11 @@ export declare abstract class Collection<M extends Model<T>, T extends object> {
66
106
  * collection.add(existingUser);
67
107
  */
68
108
  add(data: T | M): M;
109
+ /**
110
+ * Sorts the collection using the comparator if one is defined.
111
+ * Called automatically when items are added to the collection.
112
+ */
113
+ private sort;
69
114
  /**
70
115
  * Resets the collection with new data, replacing all existing items.
71
116
  *
@@ -65,8 +65,55 @@ export class Collection {
65
65
  add(data) {
66
66
  const instance = data instanceof Model ? data : new this.model(data);
67
67
  this.items.push(instance);
68
+ this.sort();
68
69
  return instance;
69
70
  }
71
+ /**
72
+ * Sorts the collection using the comparator if one is defined.
73
+ * Called automatically when items are added to the collection.
74
+ */
75
+ sort() {
76
+ if (!this.comparator) {
77
+ return;
78
+ }
79
+ const comparator = this.comparator();
80
+ if (!comparator) {
81
+ return;
82
+ }
83
+ // String attribute name
84
+ if (typeof comparator === 'string') {
85
+ const attr = comparator;
86
+ this.items.sort((a, b) => {
87
+ const aVal = a.get(attr);
88
+ const bVal = b.get(attr);
89
+ if (aVal < bVal)
90
+ return -1;
91
+ if (aVal > bVal)
92
+ return 1;
93
+ return 0;
94
+ });
95
+ return;
96
+ }
97
+ // Function comparator - check arity
98
+ if (comparator.length === 1) {
99
+ // sortBy function (single argument)
100
+ const sortByFn = comparator;
101
+ this.items.sort((a, b) => {
102
+ const aVal = sortByFn(a);
103
+ const bVal = sortByFn(b);
104
+ if (aVal < bVal)
105
+ return -1;
106
+ if (aVal > bVal)
107
+ return 1;
108
+ return 0;
109
+ });
110
+ }
111
+ else {
112
+ // sort function (two arguments)
113
+ const sortFn = comparator;
114
+ this.items.sort(sortFn);
115
+ }
116
+ }
70
117
  /**
71
118
  * Resets the collection with new data, replacing all existing items.
72
119
  *
@@ -81,6 +128,7 @@ export class Collection {
81
128
  */
82
129
  reset(data) {
83
130
  this.items = data.map((attrs) => new this.model(attrs));
131
+ this.sort();
84
132
  }
85
133
  /**
86
134
  * Finds the first item in the collection that matches the given query.
@@ -76,7 +76,7 @@ export class Model {
76
76
  * }
77
77
  * const user = new User({ _id: '507f1f77bcf86cd799439011', name: 'John' });
78
78
  */
79
- idAttribute = 'id';
79
+ idAttribute = vellumConfig.idAttribute;
80
80
  /**
81
81
  * Creates a new instance of Model.
82
82
  */
@@ -1,6 +1,7 @@
1
1
  export interface VellumConfig {
2
2
  origin: string;
3
3
  headers: Record<string, string>;
4
+ idAttribute: string;
4
5
  }
5
6
  /**
6
7
  * Global reactive state for Vellum configuration. Uses Svelte's $state rune to
@@ -8,6 +9,7 @@ export interface VellumConfig {
8
9
  *
9
10
  * @default origin - Empty string (must be configured before use)
10
11
  * @default headers - Contains 'Content-Type': 'application/json'
12
+ * @default idAttribute - The default unique identifier attribute for models
11
13
  */
12
14
  export declare const vellumConfig: VellumConfig;
13
15
  /**
@@ -20,6 +22,7 @@ export declare const vellumConfig: VellumConfig;
20
22
  * @param {Partial<VellumConfig>} config - Partial configuration object with properties to update
21
23
  * @param {string} [config.origin] - New origin URL to set
22
24
  * @param {Record<string, string>} [config.headers] - Headers to merge with existing headers
25
+ * @param {string} [config.idAttribute="id"] - The default unique identifier attribute for models
23
26
  *
24
27
  * @example
25
28
  * // Set the API origin
@@ -4,12 +4,14 @@
4
4
  *
5
5
  * @default origin - Empty string (must be configured before use)
6
6
  * @default headers - Contains 'Content-Type': 'application/json'
7
+ * @default idAttribute - The default unique identifier attribute for models
7
8
  */
8
9
  export const vellumConfig = $state({
9
10
  origin: '',
10
11
  headers: {
11
12
  'Content-Type': 'application/json'
12
- }
13
+ },
14
+ idAttribute: 'id'
13
15
  });
14
16
  /**
15
17
  * Helper function to update global Vellum configuration
@@ -21,6 +23,7 @@ export const vellumConfig = $state({
21
23
  * @param {Partial<VellumConfig>} config - Partial configuration object with properties to update
22
24
  * @param {string} [config.origin] - New origin URL to set
23
25
  * @param {Record<string, string>} [config.headers] - Headers to merge with existing headers
26
+ * @param {string} [config.idAttribute="id"] - The default unique identifier attribute for models
24
27
  *
25
28
  * @example
26
29
  * // Set the API origin
@@ -41,8 +44,12 @@ export const vellumConfig = $state({
41
44
  * });
42
45
  */
43
46
  export const configureVellum = (config) => {
44
- if (config.origin)
47
+ if (config.origin?.length) {
45
48
  vellumConfig.origin = config.origin;
49
+ }
50
+ if (config.idAttribute?.length) {
51
+ vellumConfig.idAttribute = config.idAttribute;
52
+ }
46
53
  if (config.headers) {
47
54
  vellumConfig.headers = { ...vellumConfig.headers, ...config.headers };
48
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jwerre/vellum",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Structural state management library for Svelte 5",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,7 +32,7 @@
32
32
  "types:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
33
33
  "check": "npm audit --audit-level=moderate --omit=dev && npm run format && npm run lint && npm run spell && npm run types && npm test && npm run build && npm run release:dry",
34
34
  "dev": "vite dev",
35
- "docs": "npm run build && documentation readme dist/index.js --section=\"API Documentation\"",
35
+ "docs": "npm run build && documentation readme dist/index.js --section=\"API Documentation\" && prettier --write README.md",
36
36
  "format": "prettier --check .",
37
37
  "format:write": "prettier --write .",
38
38
  "lint": "prettier --check . && eslint .",