@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 +380 -0
- package/dist/Collection.svelte.d.ts +125 -0
- package/dist/Collection.svelte.js +155 -0
- package/dist/Model.svelte.d.ts +200 -0
- package/dist/Model.svelte.js +245 -0
- package/dist/config.svelte.d.ts +9 -0
- package/dist/config.svelte.js +16 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/package.json +93 -0
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
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
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
|
+
}
|