@jwerre/vellum 0.0.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 ADDED
@@ -0,0 +1,380 @@
1
+ # Vellum
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.
4
+
5
+ ## Features
6
+
7
+ - **Rune-Powered**: Built from the ground up for Svelte 5 `$state` and `$derived`.
8
+ - **TypeScript First**: Deeply integrated generics for strict type safety across models and collections.
9
+ - **Class-Based**: Encapsulate data, validation, and API logic in clean JavaScript classes.
10
+ - **Global Config**: Centralized management for base URLs and reactive headers.
11
+ - **RESTful Persistence**: Built-in fetch, save, and destroy methods using node-fetch standards.
12
+ - **Zero Boilerplate**: No more manual $store subscriptions; just access properties directly.
13
+
14
+ ## Why Vellum?
15
+
16
+ Modern Svelte development often moves away from stores and toward raw $state objects. While flexible, this can lead to logic being scattered across components.
17
+
18
+ ### Vellum provides:
19
+
20
+ - **Consistency**: A standard way to define data entities.
21
+ - **API Integration**: A natural home for fetch, save, and delete logic.
22
+ - **Encapsulation**: Keep your data transformations inside the class, not the UI.
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm install @jwerre/vellum
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ### Configuration
33
+
34
+ Before using your models, you should configure Vellum globally. This is ideal for setting your API base URL and injecting authorization tokens. Because the configuration uses Svelte Runes, updating headers (like a bearer token) will reactively apply to all subsequent API calls.
35
+
36
+ ```ts
37
+ import { configureVellum } from '@jwerre/vellum';
38
+
39
+ configureVellum({
40
+ origin: 'https://api.example.com',
41
+ headers: {
42
+ Authorization: 'Bearer your-token-here'
43
+ }
44
+ });
45
+ ```
46
+
47
+ ### Define a Model
48
+
49
+ Extend the Model class to define your data structure and any derived state or business logic.
50
+
51
+ ```ts
52
+ import { Model } from '@jwerre/vellum';
53
+ interface UserSchema {
54
+ id: number;
55
+ firstName: string;
56
+ lastName: string;
57
+ role: 'admin' | 'user';
58
+ }
59
+
60
+ export class UserModel extends Model<UserSchema> {
61
+ endpoint() {
62
+ return `/v1/user`;
63
+ }
64
+
65
+ // Computed property using Svelte $derived
66
+ fullName = $derived(`${this.get('firstName')} ${this.get('lastName')}`);
67
+
68
+ isAdmin() {
69
+ return this.get('role') === 'admin';
70
+ }
71
+ }
72
+
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
77
+ ```
78
+
79
+ ### Define a Collection
80
+
81
+ Manage groups of models with built-in reactivity.
82
+
83
+ ```ts
84
+ import { Collection } from '@jwerre/vellum';
85
+ import { UserModel } from './UserModel.svelte.js';
86
+
87
+ export class UserCollection extends Collection<UserModel, UserSchema> {
88
+ model = UserModel;
89
+
90
+ endpoint() {
91
+ return `/v1/users`;
92
+ }
93
+
94
+ // Derived state for the entire collection
95
+ adminCount = $derived(this.items.filter((u) => u.isAdmin()).length);
96
+ }
97
+ ```
98
+
99
+ ### Use in Svelte Components
100
+
101
+ Vellum works seamlessly with Svelte 5 components.
102
+
103
+ ```svelte
104
+ <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' });
111
+ }
112
+ </script>
113
+
114
+ <h1>Admins: {users.adminCount}</h1>
115
+
116
+ <ul>
117
+ {#each users.items as user}
118
+ {#if user.isAdmin()}
119
+ <li>{user.fullName} ({user.get('email')})</li>
120
+ {/if}
121
+ {/each}
122
+ </ul>
123
+
124
+ <button onclick={addUser}>Add User</button>
125
+ ```
126
+
127
+ ## API Reference
128
+
129
+ ### `Model<T>`
130
+
131
+ The `Model` class provides a base class for creating data models with built-in CRUD operations and server synchronization.
132
+
133
+ #### Constructor
134
+
135
+ ```javascript
136
+ new Model((data = {}));
137
+ ```
138
+
139
+ Creates a new Model instance with optional initial attributes.
140
+
141
+ **Parameters:**
142
+
143
+ - `data` (Object) - Optional partial object of attributes to initialize the model
144
+
145
+ #### Abstract Properties
146
+
147
+ Must be implemented by subclasses:
148
+
149
+ - `endpoint()` - Function that returns the base URL path for API endpoints (e.g., '/users')
150
+
151
+ #### Methods
152
+
153
+ ##### get(key)
154
+
155
+ Retrieves the value of a specific attribute from the model.
156
+
157
+ **Parameters:**
158
+
159
+ - `key` - The attribute key to retrieve
160
+
161
+ **Returns:** The value associated with the specified key
162
+
163
+ ```javascript
164
+ const user = new User({ id: 1, name: 'John Doe' });
165
+ const name = user.get('name'); // Returns 'John Doe'
166
+ ```
167
+
168
+ ##### set(attrs)
169
+
170
+ Updates multiple attributes on the model instance.
171
+
172
+ **Parameters:**
173
+
174
+ - `attrs` - Partial object containing attributes to update
175
+
176
+ ```javascript
177
+ user.set({ name: 'Jane', email: 'jane@example.com' });
178
+ ```
179
+
180
+ ##### isNew()
181
+
182
+ Determines whether this model instance is new (not yet persisted).
183
+
184
+ **Returns:** `true` if the model has no ID, `false` otherwise
185
+
186
+ ```javascript
187
+ const newUser = new User({ name: 'John' });
188
+ console.log(newUser.isNew()); // true
189
+ ```
190
+
191
+ ##### sync(method, body, options)
192
+
193
+ Performs HTTP synchronization with the server for CRUD operations.
194
+
195
+ **Parameters:**
196
+
197
+ - `method` - HTTP method ('GET', 'POST', 'PUT', 'PATCH', 'DELETE'), defaults to 'GET'
198
+ - `body` - Optional request body data
199
+ - `options` - Optional configuration overrides
200
+
201
+ **Returns:** Promise resolving to server response data or null
202
+
203
+ ```javascript
204
+ // Fetch user data
205
+ const userData = await user.sync();
206
+
207
+ // Create new user
208
+ const newUser = await user.sync('POST', { name: 'John' });
209
+
210
+ // Update user
211
+ const updated = await user.sync('PUT', user.toJSON());
212
+ ```
213
+
214
+ ##### fetch()
215
+
216
+ Fetches data from the server and updates the model's attributes.
217
+
218
+ **Returns:** Promise
219
+
220
+ ```javascript
221
+ const user = new User({ id: 1 });
222
+ await user.fetch(); // Model now contains full user data
223
+ ```
224
+
225
+ ##### save()
226
+
227
+ Saves the model by creating (POST) or updating (PUT) the server resource.
228
+
229
+ **Returns:** Promise
230
+
231
+ ```javascript
232
+ // Create new user
233
+ const newUser = new User({ name: 'John' });
234
+ await newUser.save(); // POST request
235
+
236
+ // Update existing user
237
+ user.set({ name: 'Jane' });
238
+ await user.save(); // PUT request
239
+ ```
240
+
241
+ ##### destroy()
242
+
243
+ Deletes the model from the server.
244
+
245
+ **Returns:** Promise
246
+
247
+ ```javascript
248
+ await user.destroy(); // DELETE request to /users/1
249
+ ```
250
+
251
+ ##### toJSON()
252
+
253
+ Returns a plain object representation of the model's attributes.
254
+
255
+ **Returns:** Plain object containing all attributes
256
+
257
+ ```javascript
258
+ const user = new User({ id: 1, name: 'John' });
259
+ const userData = user.toJSON();
260
+ // Returns: { id: 1, name: 'John' }
261
+ ```
262
+
263
+ #### Example Usage
264
+
265
+ ```javascript
266
+ class User extends Model {
267
+ endpoint() {
268
+ return '/users';
269
+ }
270
+ }
271
+
272
+ // Create and save new user
273
+ const user = new User({ name: 'John', email: 'john@example.com' });
274
+ await user.save();
275
+
276
+ // Fetch existing user
277
+ const existingUser = new User({ id: 1 });
278
+ await existingUser.fetch();
279
+
280
+ // Update user
281
+ existingUser.set({ name: 'Jane' });
282
+ await existingUser.save();
283
+
284
+ // Delete user
285
+ await existingUser.destroy();
286
+ ```
287
+
288
+ ### `Collection<M, T>`
289
+
290
+ The `Collection` class provides a reactive container for managing groups of Model instances with automatic UI updates.
291
+
292
+ #### Constructor
293
+
294
+ ```javascript
295
+ new Collection((models = []));
296
+ ```
297
+
298
+ Creates a new Collection instance with optional initial data.
299
+
300
+ **Parameters:**
301
+
302
+ - `models` (Array) - Optional array of data objects to initialize the collection
303
+
304
+ #### Properties
305
+
306
+ - `items` - Reactive array of model instances in the collection
307
+ - `length` - Number of items in the collection (read-only)
308
+
309
+ #### Abstract Properties
310
+
311
+ These must be implemented by subclasses:
312
+
313
+ - `model` - The Model class constructor for creating instances
314
+ - `endpoint()` - Function that returns the API endpoint URL
315
+
316
+ #### Methods
317
+
318
+ ##### add(data)
319
+
320
+ Adds a new item to the collection.
321
+
322
+ **Parameters:**
323
+
324
+ - `data` - Raw data object or existing model instance
325
+
326
+ **Returns:** The model instance that was added
327
+
328
+ ```javascript
329
+ const user = collection.add({ name: 'John', email: 'john@example.com' });
330
+ ```
331
+
332
+ ##### reset(data)
333
+
334
+ Replaces all items in the collection with new data.
335
+
336
+ **Parameters:**
337
+
338
+ - `data` - Array of raw data objects
339
+
340
+ ```javascript
341
+ collection.reset([
342
+ { id: 1, name: 'John' },
343
+ { id: 2, name: 'Jane' }
344
+ ]);
345
+ ```
346
+
347
+ ##### find(query)
348
+
349
+ Finds the first item matching the query object.
350
+
351
+ **Parameters:**
352
+
353
+ - `query` - Object with key-value pairs to match
354
+
355
+ **Returns:** The first matching item or `undefined`
356
+
357
+ ```javascript
358
+ const user = collection.find({ id: 123 });
359
+ const activeAdmin = collection.find({ role: 'admin', status: 'active' });
360
+ ```
361
+
362
+ ##### fetch(options)
363
+
364
+ Fetches data from the server and populates the collection.
365
+
366
+ **Parameters:**
367
+
368
+ - `options.search` - Optional search parameters for the query string
369
+
370
+ **Returns:** Promise
371
+
372
+ ```javascript
373
+ // Fetch all items
374
+ await collection.fetch();
375
+
376
+ // Fetch with search parameters
377
+ await collection.fetch({
378
+ search: { limit: 30, after: 29 }
379
+ });
380
+ ```
@@ -0,0 +1,125 @@
1
+ import { Model } from './Model.svelte';
2
+ /**
3
+ * Abstract base class for managing collections of Model instances.
4
+ *
5
+ * Provides a reactive collection that can be populated with data, fetched from a server,
6
+ * and manipulated with type-safe operations. The collection is backed by Svelte's reactivity
7
+ * system for automatic UI updates.
8
+ *
9
+ * @template M - The Model type that extends Model<T>
10
+ * @template T - The data object type that the models represent
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * class UserCollection extends Collection<UserModel, User> {
15
+ * model = UserModel;
16
+ * endpoint = () => '/api/users';
17
+ * }
18
+ *
19
+ * const users = new UserCollection();
20
+ * await users.fetch(); // Loads users from API
21
+ * users.add({ name: 'John', email: 'john@example.com' }); // Adds new user
22
+ * ```
23
+ */
24
+ export declare abstract class Collection<M extends Model<T>, T extends object> {
25
+ /** Reactive array of model instances in the collection */
26
+ items: M[];
27
+ /** The Model class constructor used to create new instances */
28
+ abstract model: {
29
+ new (data: Partial<T>): M;
30
+ };
31
+ /** Returns the API endpoint URL for this collection */
32
+ abstract endpoint(): string;
33
+ /**
34
+ * Creates a new Collection instance.
35
+ *
36
+ * @param models - Optional array of data objects to initialize the collection with
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * // Create empty collection
41
+ * const collection = new UserCollection();
42
+ *
43
+ * // Create collection with initial data
44
+ * const collection = new UserCollection([
45
+ * { id: 1, name: 'John' },
46
+ * { id: 2, name: 'Jane' }
47
+ * ]);
48
+ * ```
49
+ */
50
+ constructor(models?: T[]);
51
+ /** Gets the number of items in the collection */
52
+ get length(): number;
53
+ /**
54
+ * Adds a new item to the collection.
55
+ *
56
+ * @param data - Either raw data of type T or an existing model instance of type M
57
+ * @returns The model instance that was added to the collection
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * // Add raw data
62
+ * const user = collection.add({ name: 'John', email: 'john@example.com' });
63
+ *
64
+ * // Add existing model instance
65
+ * const existingUser = new UserModel({ name: 'Jane' });
66
+ * collection.add(existingUser);
67
+ * ```
68
+ */
69
+ add(data: T | M): M;
70
+ /**
71
+ * Resets the collection with new data, replacing all existing items.
72
+ *
73
+ * @param data - An array of raw data objects to populate the collection with
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * // Reset collection with new user data
78
+ * collection.reset([
79
+ * { id: 1, name: 'John', email: 'john@example.com' },
80
+ * { id: 2, name: 'Jane', email: 'jane@example.com' }
81
+ * ]);
82
+ * ```
83
+ */
84
+ reset(data: T[]): void;
85
+ /**
86
+ * Finds the first item in the collection that matches the given query.
87
+ *
88
+ * @param query - An object containing key-value pairs to match against items in the collection.
89
+ * Only items that match all specified properties will be returned.
90
+ * @returns The first matching item, or undefined if no match is found.
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * // Find a user by ID
95
+ * const user = collection.find({ id: 123 });
96
+ *
97
+ * // Find by multiple properties
98
+ * const activeAdmin = collection.find({ role: 'admin', status: 'active' });
99
+ * ```
100
+ */
101
+ find(query: Partial<T>): M | undefined;
102
+ /**
103
+ * Fetches data from the server and populates the collection.
104
+ *
105
+ * @param options - Configuration options for the fetch request
106
+ * @param options.search - Optional search parameters to include in the query string.
107
+ * Keys and values will be converted to strings and URL-encoded.
108
+ *
109
+ * @throws {Error} Throws an error if the HTTP request fails or returns a non-ok status
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * // Fetch all items
114
+ * await collection.fetch();
115
+ *
116
+ * // Fetch with search parameters
117
+ * await collection.fetch({
118
+ * search: { limit: 30, after: 29 }
119
+ * });
120
+ * ```
121
+ */
122
+ fetch(options?: {
123
+ search?: Record<string, string | number | boolean>;
124
+ }): Promise<void>;
125
+ }
@@ -0,0 +1,155 @@
1
+ import { SvelteURLSearchParams } from 'svelte/reactivity';
2
+ import { Model } from './Model.svelte';
3
+ import { vellumConfig } from './config.svelte';
4
+ /**
5
+ * Abstract base class for managing collections of Model instances.
6
+ *
7
+ * Provides a reactive collection that can be populated with data, fetched from a server,
8
+ * and manipulated with type-safe operations. The collection is backed by Svelte's reactivity
9
+ * system for automatic UI updates.
10
+ *
11
+ * @template M - The Model type that extends Model<T>
12
+ * @template T - The data object type that the models represent
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * class UserCollection extends Collection<UserModel, User> {
17
+ * model = UserModel;
18
+ * endpoint = () => '/api/users';
19
+ * }
20
+ *
21
+ * const users = new UserCollection();
22
+ * await users.fetch(); // Loads users from API
23
+ * users.add({ name: 'John', email: 'john@example.com' }); // Adds new user
24
+ * ```
25
+ */
26
+ export class Collection {
27
+ /** Reactive array of model instances in the collection */
28
+ items = $state([]);
29
+ /**
30
+ * Creates a new Collection instance.
31
+ *
32
+ * @param models - Optional array of data objects to initialize the collection with
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * // Create empty collection
37
+ * const collection = new UserCollection();
38
+ *
39
+ * // Create collection with initial data
40
+ * const collection = new UserCollection([
41
+ * { id: 1, name: 'John' },
42
+ * { id: 2, name: 'Jane' }
43
+ * ]);
44
+ * ```
45
+ */
46
+ constructor(models = []) {
47
+ if (models.length > 0) {
48
+ this.reset(models);
49
+ }
50
+ }
51
+ /** Gets the number of items in the collection */
52
+ get length() {
53
+ return this.items.length;
54
+ }
55
+ /**
56
+ * Adds a new item to the collection.
57
+ *
58
+ * @param data - Either raw data of type T or an existing model instance of type M
59
+ * @returns The model instance that was added to the collection
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * // Add raw data
64
+ * const user = collection.add({ name: 'John', email: 'john@example.com' });
65
+ *
66
+ * // Add existing model instance
67
+ * const existingUser = new UserModel({ name: 'Jane' });
68
+ * collection.add(existingUser);
69
+ * ```
70
+ */
71
+ add(data) {
72
+ const instance = data instanceof Model ? data : new this.model(data);
73
+ this.items.push(instance);
74
+ return instance;
75
+ }
76
+ /**
77
+ * Resets the collection with new data, replacing all existing items.
78
+ *
79
+ * @param data - An array of raw data objects to populate the collection with
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * // Reset collection with new user data
84
+ * collection.reset([
85
+ * { id: 1, name: 'John', email: 'john@example.com' },
86
+ * { id: 2, name: 'Jane', email: 'jane@example.com' }
87
+ * ]);
88
+ * ```
89
+ */
90
+ reset(data) {
91
+ this.items = data.map((attrs) => new this.model(attrs));
92
+ }
93
+ /**
94
+ * Finds the first item in the collection that matches the given query.
95
+ *
96
+ * @param query - An object containing key-value pairs to match against items in the collection.
97
+ * Only items that match all specified properties will be returned.
98
+ * @returns The first matching item, or undefined if no match is found.
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * // Find a user by ID
103
+ * const user = collection.find({ id: 123 });
104
+ *
105
+ * // Find by multiple properties
106
+ * const activeAdmin = collection.find({ role: 'admin', status: 'active' });
107
+ * ```
108
+ */
109
+ find(query) {
110
+ return this.items.find((item) => {
111
+ return Object.entries(query).every(([key, value]) => {
112
+ return item.get(key) === value;
113
+ });
114
+ });
115
+ }
116
+ /**
117
+ * Fetches data from the server and populates the collection.
118
+ *
119
+ * @param options - Configuration options for the fetch request
120
+ * @param options.search - Optional search parameters to include in the query string.
121
+ * Keys and values will be converted to strings and URL-encoded.
122
+ *
123
+ * @throws {Error} Throws an error if the HTTP request fails or returns a non-ok status
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * // Fetch all items
128
+ * await collection.fetch();
129
+ *
130
+ * // Fetch with search parameters
131
+ * await collection.fetch({
132
+ * search: { limit: 30, after: 29 }
133
+ * });
134
+ * ```
135
+ */
136
+ async fetch(options = {}) {
137
+ let query = '';
138
+ if (options.search) {
139
+ const params = new SvelteURLSearchParams();
140
+ for (const [key, value] of Object.entries(options.search)) {
141
+ params.append(key, String(value));
142
+ }
143
+ query = `?${params.toString()}`;
144
+ }
145
+ const fullUrl = `${vellumConfig.origin}${this.endpoint()}${query}`;
146
+ const response = await fetch(fullUrl, {
147
+ headers: { ...vellumConfig.headers }
148
+ });
149
+ if (!response.ok) {
150
+ throw new Error(`Vellum Collection Error: ${response.statusText}`);
151
+ }
152
+ const data = (await response.json());
153
+ this.reset(data);
154
+ }
155
+ }
@@ -0,0 +1,200 @@
1
+ import { type VellumConfig } from './config.svelte';
2
+ export declare abstract class Model<T extends object> {
3
+ #private;
4
+ /**
5
+ * Abstract method that must be implemented by subclasses to define the base URL path
6
+ * for API endpoints related to this model.
7
+ *
8
+ * This method returns the root URL segment that will be appended to the base API URL
9
+ * to form complete endpoints for CRUD operations. For example, if endpoint() returns
10
+ * '/users', the full URL for API calls would be `${baseUrl}/users` for collections
11
+ * or `${baseUrl}/users/{id}` for individual resources.
12
+ *
13
+ * @returns {string} The root URL path for this model's API endpoints (e.g., '/users', '/posts')
14
+ * @example
15
+ * // In a User model subclass:
16
+ * endpoint(): string {
17
+ * return '/users';
18
+ * }
19
+ */
20
+ abstract endpoint(): string;
21
+ constructor(data?: Partial<T>);
22
+ /**
23
+ * Retrieves the value of a specific attribute from the model.
24
+ *
25
+ * This method provides type-safe access to model attributes, ensuring that the
26
+ * returned value matches the expected type for the given key. It acts as a
27
+ * getter for the internal attributes stored in the model instance.
28
+ *
29
+ * @template K - The key type, constrained to keys of T
30
+ * @param {K} key - The attribute key to retrieve the value for
31
+ * @returns {T[K]} The value associated with the specified key
32
+ * @example
33
+ * // Assuming a User model with attributes { id: number, name: string }
34
+ * const user = new User({ id: 1, name: 'John Doe' });
35
+ * const name = user.get('name'); // Returns 'John Doe' (string)
36
+ * const id = user.get('id'); // Returns 1 (number)
37
+ */
38
+ get<K extends keyof T>(key: K): T[K];
39
+ /**
40
+ * Sets multiple attributes on the model instance.
41
+ *
42
+ * This method allows for bulk updating of model attributes by merging the provided
43
+ * partial attributes object with the existing attributes. The method performs a
44
+ * shallow merge, meaning that only the top-level properties specified in the attrs
45
+ * parameter will be updated, while other existing attributes remain unchanged.
46
+ *
47
+ * @param {Partial<T>} attrs - A partial object containing the attributes to update
48
+ * @returns {void}
49
+ * @example
50
+ * // Assuming a User model with attributes { id: number, name: string, email: string }
51
+ * const user = new User({ id: 1, name: 'John', email: 'john@example.com' });
52
+ *
53
+ * // Update multiple attributes at once
54
+ * user.set({ name: 'Jane', email: 'jane@example.com' });
55
+ * // Now user has { id: 1, name: 'Jane', email: 'jane@example.com' }
56
+ *
57
+ * // Update a single attribute
58
+ * user.set({ name: 'Bob' });
59
+ * // Now user has { id: 1, name: 'Bob', email: 'jane@example.com' }
60
+ */
61
+ set(attrs: Partial<T>): void;
62
+ /**
63
+ * Determines whether this model instance is new (not yet persisted).
64
+ * A model is considered new if it doesn't have an 'id' or '_id' attribute.
65
+ *
66
+ * @returns {boolean} true if the model is new, false if it has been persisted
67
+ */
68
+ isNew(): boolean;
69
+ /**
70
+ * Performs HTTP synchronization with the server for CRUD operations.
71
+ *
72
+ * This method handles all HTTP communication between the model and the server,
73
+ * automatically constructing the appropriate URL based on the model's ID and
74
+ * endpoint(). It supports all standard REST operations and provides type-safe
75
+ * response handling.
76
+ *
77
+ * The URL construction follows REST conventions:
78
+ * - For new models (no ID): uses collection endpoint `${baseUrl}${endpoint()}`
79
+ * - For existing models (with ID): uses resource endpoint `${baseUrl}${endpoint()}/${id}`
80
+ *
81
+ * @template R - The expected response type, defaults to T (the model's attribute type)
82
+ * @param {('GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE')} [method='GET'] - The HTTP method to use (defaults to 'GET')
83
+ * @param {Record<string, unknown> | T} [body] - Optional request body data to send
84
+ * @returns {Promise<R | null>} The server response data, or null for 204 No Content responses
85
+ * @throws {Error} Throws an error if the HTTP response is not successful
86
+ *
87
+ * @example
88
+ * // Fetch a user by ID (default 'GET' request)
89
+ * const userData = await user.sync();
90
+ *
91
+ * // Create a new user (POST request)
92
+ * const newUser = await user.sync('POST', { name: 'John', email: 'john@example.com' });
93
+ *
94
+ * // Update an existing user (PUT request)
95
+ * const updatedUser = await user.sync('PUT', user.toJSON());
96
+ *
97
+ * // Delete a user (DELETE request)
98
+ * await user.sync('DELETE'); // Returns null for 204 responses
99
+ */
100
+ sync<R = T>(method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE', body?: Record<string, unknown> | T, options?: VellumConfig): Promise<R | null>;
101
+ /**
102
+ * Fetches data from the server and updates the model's attributes.
103
+ *
104
+ * This method performs a GET request to retrieve the latest data for this model
105
+ * instance from the server. If the model has an ID, it will fetch the specific
106
+ * resource; if it's a new model without an ID, it will make a request to the
107
+ * collection endpoint.
108
+ *
109
+ * Upon successful retrieval, the model's attributes are automatically updated
110
+ * with the server response data. This method is useful for refreshing a model's
111
+ * state or loading data after creating a model instance with just an ID.
112
+ *
113
+ * @returns {Promise<void>} A promise that resolves when the fetch operation completes
114
+ * @throws {Error} Throws an error if the HTTP request fails or server returns an error
115
+ *
116
+ * @example
117
+ * // Fetch data for an existing user
118
+ * const user = new User({ id: 1 });
119
+ * await user.fetch(); // Model now contains full user data from server
120
+ *
121
+ * // Refresh a model's data
122
+ * await existingUser.fetch(); // Updates with latest server data
123
+ */
124
+ fetch(): Promise<void>;
125
+ /**
126
+ * Saves the model to the server by creating a new resource or updating an existing one.
127
+ *
128
+ * This method automatically determines whether to create or update based on the model's
129
+ * state. If the model is new (has no ID), it performs a POST request to create a new
130
+ * resource. If the model already exists (has an ID), it performs a PUT request to
131
+ * update the existing resource.
132
+ *
133
+ * After a successful save operation, the model's attributes are updated with any
134
+ * data returned from the server. This is particularly useful when the server
135
+ * generates additional fields (like timestamps, computed values, or normalized data)
136
+ * during the save process.
137
+ *
138
+ * @returns {Promise<void>} A promise that resolves when the save operation completes
139
+ * @throws {Error} Throws an error if the HTTP request fails or server returns an error
140
+ *
141
+ * @example
142
+ * // Create a new user
143
+ * const newUser = new User({ name: 'John', email: 'john@example.com' });
144
+ * await newUser.save(); // POST request, user now has ID from server
145
+ *
146
+ * // Update an existing user
147
+ * existingUser.set({ name: 'Jane' });
148
+ * await existingUser.save(); // PUT request with updated data
149
+ */
150
+ save(): Promise<void>;
151
+ /**
152
+ * Deletes the model from the server.
153
+ *
154
+ * This method performs a DELETE request to remove the model's corresponding resource
155
+ * from the server. The method only executes if the model has an ID (i.e., it exists
156
+ * on the server). If the model is new and has no ID, the method will return without
157
+ * performing any operation.
158
+ *
159
+ * The DELETE request is sent to the model's specific resource endpoint using the
160
+ * pattern `${baseUrl}${endpoint()}/${id}`. After successful deletion, the model
161
+ * instance remains in memory but the corresponding server resource is removed.
162
+ *
163
+ * @returns {Promise<void>} A promise that resolves when the delete operation completes
164
+ * @throws {Error} Throws an error if the HTTP request fails or server returns an error
165
+ *
166
+ * @example
167
+ * // Delete an existing user
168
+ * const user = new User({ id: 1, name: 'John' });
169
+ * await user.destroy(); // DELETE request to /users/1
170
+ *
171
+ * // Attempting to destroy a new model (no operation performed)
172
+ * const newUser = new User({ name: 'Jane' }); // No ID
173
+ * await newUser.destroy(); // Returns immediately, no HTTP request
174
+ */
175
+ destroy(): Promise<void>;
176
+ /**
177
+ * Returns a plain JavaScript object representation of the model's attributes.
178
+ *
179
+ * This method creates a shallow copy of the model's internal attributes, returning
180
+ * them as a plain object. This is useful for serialization, debugging, or when you
181
+ * need to pass the model's data to functions that expect plain objects rather than
182
+ * model instances.
183
+ *
184
+ * The returned object is a copy, so modifications to it will not affect the original
185
+ * model's attributes. This method is commonly used internally by other model methods
186
+ * (like save()) when preparing data for HTTP requests.
187
+ *
188
+ * @returns {T} A plain object containing all of the model's attributes
189
+ *
190
+ * @example
191
+ * // Get plain object representation
192
+ * const user = new User({ id: 1, name: 'John', email: 'john@example.com' });
193
+ * const userData = user.toJSON();
194
+ * // Returns: { id: 1, name: 'John', email: 'john@example.com' }
195
+ *
196
+ * // Useful for serialization
197
+ * const jsonString = JSON.stringify(user.toJSON());
198
+ */
199
+ toJSON(): T;
200
+ }
@@ -0,0 +1,245 @@
1
+ import { vellumConfig } from './config.svelte';
2
+ export class Model {
3
+ #attributes = $state({});
4
+ constructor(data = {}) {
5
+ this.#attributes = { ...data };
6
+ }
7
+ /**
8
+ * Internal helper to find the ID
9
+ */
10
+ #getId() {
11
+ // Cast to Record<string, unknown> to allow string indexing
12
+ const attrs = this.#attributes;
13
+ const id = attrs['id'] ?? attrs['_id'];
14
+ if (typeof id === 'string' || typeof id === 'number') {
15
+ return id;
16
+ }
17
+ return undefined;
18
+ }
19
+ /**
20
+ * Retrieves the value of a specific attribute from the model.
21
+ *
22
+ * This method provides type-safe access to model attributes, ensuring that the
23
+ * returned value matches the expected type for the given key. It acts as a
24
+ * getter for the internal attributes stored in the model instance.
25
+ *
26
+ * @template K - The key type, constrained to keys of T
27
+ * @param {K} key - The attribute key to retrieve the value for
28
+ * @returns {T[K]} The value associated with the specified key
29
+ * @example
30
+ * // Assuming a User model with attributes { id: number, name: string }
31
+ * const user = new User({ id: 1, name: 'John Doe' });
32
+ * const name = user.get('name'); // Returns 'John Doe' (string)
33
+ * const id = user.get('id'); // Returns 1 (number)
34
+ */
35
+ get(key) {
36
+ return this.#attributes[key];
37
+ }
38
+ /**
39
+ * Sets multiple attributes on the model instance.
40
+ *
41
+ * This method allows for bulk updating of model attributes by merging the provided
42
+ * partial attributes object with the existing attributes. The method performs a
43
+ * shallow merge, meaning that only the top-level properties specified in the attrs
44
+ * parameter will be updated, while other existing attributes remain unchanged.
45
+ *
46
+ * @param {Partial<T>} attrs - A partial object containing the attributes to update
47
+ * @returns {void}
48
+ * @example
49
+ * // Assuming a User model with attributes { id: number, name: string, email: string }
50
+ * const user = new User({ id: 1, name: 'John', email: 'john@example.com' });
51
+ *
52
+ * // Update multiple attributes at once
53
+ * user.set({ name: 'Jane', email: 'jane@example.com' });
54
+ * // Now user has { id: 1, name: 'Jane', email: 'jane@example.com' }
55
+ *
56
+ * // Update a single attribute
57
+ * user.set({ name: 'Bob' });
58
+ * // Now user has { id: 1, name: 'Bob', email: 'jane@example.com' }
59
+ */
60
+ set(attrs) {
61
+ Object.assign(this.#attributes, attrs);
62
+ }
63
+ /**
64
+ * Determines whether this model instance is new (not yet persisted).
65
+ * A model is considered new if it doesn't have an 'id' or '_id' attribute.
66
+ *
67
+ * @returns {boolean} true if the model is new, false if it has been persisted
68
+ */
69
+ isNew() {
70
+ return !this.#getId();
71
+ }
72
+ /**
73
+ * Performs HTTP synchronization with the server for CRUD operations.
74
+ *
75
+ * This method handles all HTTP communication between the model and the server,
76
+ * automatically constructing the appropriate URL based on the model's ID and
77
+ * endpoint(). It supports all standard REST operations and provides type-safe
78
+ * response handling.
79
+ *
80
+ * The URL construction follows REST conventions:
81
+ * - For new models (no ID): uses collection endpoint `${baseUrl}${endpoint()}`
82
+ * - For existing models (with ID): uses resource endpoint `${baseUrl}${endpoint()}/${id}`
83
+ *
84
+ * @template R - The expected response type, defaults to T (the model's attribute type)
85
+ * @param {('GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE')} [method='GET'] - The HTTP method to use (defaults to 'GET')
86
+ * @param {Record<string, unknown> | T} [body] - Optional request body data to send
87
+ * @returns {Promise<R | null>} The server response data, or null for 204 No Content responses
88
+ * @throws {Error} Throws an error if the HTTP response is not successful
89
+ *
90
+ * @example
91
+ * // Fetch a user by ID (default 'GET' request)
92
+ * const userData = await user.sync();
93
+ *
94
+ * // Create a new user (POST request)
95
+ * const newUser = await user.sync('POST', { name: 'John', email: 'john@example.com' });
96
+ *
97
+ * // Update an existing user (PUT request)
98
+ * const updatedUser = await user.sync('PUT', user.toJSON());
99
+ *
100
+ * // Delete a user (DELETE request)
101
+ * await user.sync('DELETE'); // Returns null for 204 responses
102
+ */
103
+ async sync(method = 'GET', body, options) {
104
+ const id = this.#getId();
105
+ const fullUrl = `${vellumConfig.origin}${this.endpoint()}`;
106
+ const url = id ? `${fullUrl}/${id}` : fullUrl;
107
+ const fetchOpts = {
108
+ method,
109
+ headers: {
110
+ ...vellumConfig.headers,
111
+ ...options?.headers
112
+ },
113
+ body: body ? JSON.stringify(body) : undefined
114
+ };
115
+ // console.log('Model::sync()', url, fetchOpts);
116
+ const response = await fetch(url, fetchOpts);
117
+ if (!response.ok) {
118
+ throw new Error(`Vellum Sync Error: ${response.statusText}`);
119
+ }
120
+ // Handle 204 No Content safely
121
+ if (response.status === 204) {
122
+ return null;
123
+ }
124
+ const data = await response.json();
125
+ return data;
126
+ }
127
+ /**
128
+ * Fetches data from the server and updates the model's attributes.
129
+ *
130
+ * This method performs a GET request to retrieve the latest data for this model
131
+ * instance from the server. If the model has an ID, it will fetch the specific
132
+ * resource; if it's a new model without an ID, it will make a request to the
133
+ * collection endpoint.
134
+ *
135
+ * Upon successful retrieval, the model's attributes are automatically updated
136
+ * with the server response data. This method is useful for refreshing a model's
137
+ * state or loading data after creating a model instance with just an ID.
138
+ *
139
+ * @returns {Promise<void>} A promise that resolves when the fetch operation completes
140
+ * @throws {Error} Throws an error if the HTTP request fails or server returns an error
141
+ *
142
+ * @example
143
+ * // Fetch data for an existing user
144
+ * const user = new User({ id: 1 });
145
+ * await user.fetch(); // Model now contains full user data from server
146
+ *
147
+ * // Refresh a model's data
148
+ * await existingUser.fetch(); // Updates with latest server data
149
+ */
150
+ async fetch() {
151
+ const data = await this.sync('GET');
152
+ if (data && typeof data === 'object') {
153
+ this.set(data);
154
+ }
155
+ }
156
+ /**
157
+ * Saves the model to the server by creating a new resource or updating an existing one.
158
+ *
159
+ * This method automatically determines whether to create or update based on the model's
160
+ * state. If the model is new (has no ID), it performs a POST request to create a new
161
+ * resource. If the model already exists (has an ID), it performs a PUT request to
162
+ * update the existing resource.
163
+ *
164
+ * After a successful save operation, the model's attributes are updated with any
165
+ * data returned from the server. This is particularly useful when the server
166
+ * generates additional fields (like timestamps, computed values, or normalized data)
167
+ * during the save process.
168
+ *
169
+ * @returns {Promise<void>} A promise that resolves when the save operation completes
170
+ * @throws {Error} Throws an error if the HTTP request fails or server returns an error
171
+ *
172
+ * @example
173
+ * // Create a new user
174
+ * const newUser = new User({ name: 'John', email: 'john@example.com' });
175
+ * await newUser.save(); // POST request, user now has ID from server
176
+ *
177
+ * // Update an existing user
178
+ * existingUser.set({ name: 'Jane' });
179
+ * await existingUser.save(); // PUT request with updated data
180
+ */
181
+ async save() {
182
+ const id = this.#getId();
183
+ const method = id ? 'PUT' : 'POST';
184
+ const data = await this.sync(method, this.toJSON());
185
+ if (data && typeof data === 'object') {
186
+ this.set(data);
187
+ }
188
+ }
189
+ /**
190
+ * Deletes the model from the server.
191
+ *
192
+ * This method performs a DELETE request to remove the model's corresponding resource
193
+ * from the server. The method only executes if the model has an ID (i.e., it exists
194
+ * on the server). If the model is new and has no ID, the method will return without
195
+ * performing any operation.
196
+ *
197
+ * The DELETE request is sent to the model's specific resource endpoint using the
198
+ * pattern `${baseUrl}${endpoint()}/${id}`. After successful deletion, the model
199
+ * instance remains in memory but the corresponding server resource is removed.
200
+ *
201
+ * @returns {Promise<void>} A promise that resolves when the delete operation completes
202
+ * @throws {Error} Throws an error if the HTTP request fails or server returns an error
203
+ *
204
+ * @example
205
+ * // Delete an existing user
206
+ * const user = new User({ id: 1, name: 'John' });
207
+ * await user.destroy(); // DELETE request to /users/1
208
+ *
209
+ * // Attempting to destroy a new model (no operation performed)
210
+ * const newUser = new User({ name: 'Jane' }); // No ID
211
+ * await newUser.destroy(); // Returns immediately, no HTTP request
212
+ */
213
+ async destroy() {
214
+ const id = this.#getId();
215
+ if (id) {
216
+ await this.sync('DELETE');
217
+ }
218
+ }
219
+ /**
220
+ * Returns a plain JavaScript object representation of the model's attributes.
221
+ *
222
+ * This method creates a shallow copy of the model's internal attributes, returning
223
+ * them as a plain object. This is useful for serialization, debugging, or when you
224
+ * need to pass the model's data to functions that expect plain objects rather than
225
+ * model instances.
226
+ *
227
+ * The returned object is a copy, so modifications to it will not affect the original
228
+ * model's attributes. This method is commonly used internally by other model methods
229
+ * (like save()) when preparing data for HTTP requests.
230
+ *
231
+ * @returns {T} A plain object containing all of the model's attributes
232
+ *
233
+ * @example
234
+ * // Get plain object representation
235
+ * const user = new User({ id: 1, name: 'John', email: 'john@example.com' });
236
+ * const userData = user.toJSON();
237
+ * // Returns: { id: 1, name: 'John', email: 'john@example.com' }
238
+ *
239
+ * // Useful for serialization
240
+ * const jsonString = JSON.stringify(user.toJSON());
241
+ */
242
+ toJSON() {
243
+ return { ...this.#attributes };
244
+ }
245
+ }
@@ -0,0 +1,9 @@
1
+ export interface VellumConfig {
2
+ origin: string;
3
+ headers: Record<string, string>;
4
+ }
5
+ export declare const vellumConfig: VellumConfig;
6
+ /**
7
+ * Helper to update global configuration
8
+ */
9
+ export declare const configureVellum: (config: Partial<VellumConfig>) => void;
@@ -0,0 +1,16 @@
1
+ export const vellumConfig = $state({
2
+ origin: '',
3
+ headers: {
4
+ 'Content-Type': 'application/json'
5
+ }
6
+ });
7
+ /**
8
+ * Helper to update global configuration
9
+ */
10
+ export const configureVellum = (config) => {
11
+ if (config.origin)
12
+ vellumConfig.origin = config.origin;
13
+ if (config.headers) {
14
+ vellumConfig.headers = { ...vellumConfig.headers, ...config.headers };
15
+ }
16
+ };
@@ -0,0 +1,3 @@
1
+ export { vellumConfig, configureVellum } from './config.svelte.js';
2
+ export { Model } from './Model.svelte.js';
3
+ export { Collection } from './Collection.svelte.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // Reexport your entry components here
2
+ export { vellumConfig, configureVellum } from './config.svelte.js';
3
+ export { Model } from './Model.svelte.js';
4
+ export { Collection } from './Collection.svelte.js';
package/package.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "name": "@jwerre/vellum",
3
+ "version": "0.0.1",
4
+ "description": "Structural state management library for Svelte 5",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git@github.com:jwerre/vellum.git"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "keywords": [
13
+ "svelte",
14
+ "svelte5",
15
+ "runes",
16
+ "vellum",
17
+ "model",
18
+ "collection",
19
+ "state-management",
20
+ "typescript",
21
+ "rest",
22
+ "active-record",
23
+ "orm",
24
+ "reactive",
25
+ "data-modeling"
26
+ ],
27
+ "author": "Jonah Werre <jonahwerre@gmail.com>",
28
+ "scripts": {
29
+ "build": "svelte-kit sync && svelte-package",
30
+ "prepack": "publint",
31
+ "types": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
32
+ "types:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
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
+ "dev": "vite dev",
35
+ "format": "prettier --check .",
36
+ "format:write": "prettier --write .",
37
+ "lint": "prettier --check . && eslint .",
38
+ "prepare": "svelte-kit sync || echo ''",
39
+ "preview": "vite preview",
40
+ "release": "semantic-release",
41
+ "release:dry": "semantic-release --dry-run",
42
+ "spell": "cspell .",
43
+ "test": "vitest --run",
44
+ "test:watch": "vitest"
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "!dist/**/*.test.*",
49
+ "!dist/**/*.spec.*"
50
+ ],
51
+ "sideEffects": false,
52
+ "svelte": "./dist/index.js",
53
+ "types": "./dist/index.d.ts",
54
+ "type": "module",
55
+ "exports": {
56
+ ".": {
57
+ "types": "./dist/index.d.ts",
58
+ "svelte": "./dist/index.js"
59
+ }
60
+ },
61
+ "peerDependencies": {
62
+ "svelte": "^5.0.0"
63
+ },
64
+ "devDependencies": {
65
+ "@eslint/compat": "^1.4.0",
66
+ "@eslint/js": "^9.39.1",
67
+ "@semantic-release/changelog": "^6.0.3",
68
+ "@semantic-release/commit-analyzer": "^13.0.1",
69
+ "@semantic-release/github": "^12.0.2",
70
+ "@semantic-release/npm": "^13.1.3",
71
+ "@semantic-release/release-notes-generator": "^14.1.0",
72
+ "@sveltejs/adapter-auto": "^7.0.0",
73
+ "@sveltejs/kit": "^2.49.1",
74
+ "@sveltejs/package": "^2.5.7",
75
+ "@sveltejs/vite-plugin-svelte": "^6.2.1",
76
+ "@types/node": "^24",
77
+ "cspell": "^9.4.0",
78
+ "eslint": "^9.39.1",
79
+ "eslint-config-prettier": "^10.1.8",
80
+ "eslint-plugin-svelte": "^3.13.1",
81
+ "globals": "^16.5.0",
82
+ "prettier": "^3.7.4",
83
+ "prettier-plugin-svelte": "^3.4.0",
84
+ "publint": "^0.3.15",
85
+ "semantic-release": "^25.0.2",
86
+ "svelte": "^5.45.6",
87
+ "svelte-check": "^4.3.4",
88
+ "typescript": "^5.9.3",
89
+ "typescript-eslint": "^8.48.1",
90
+ "vite": "^7.2.6",
91
+ "vitest": "^4.0.15"
92
+ }
93
+ }