@quvel-kit/core 1.3.21 → 1.3.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -43,6 +43,7 @@ export * from './utils/scripts.js';
43
43
  export * from './utils/assets.js';
44
44
  export * from './utils/pagination.js';
45
45
  export * from './orm/index.js';
46
+ export { Model } from './orm/Model.js';
46
47
  export { defineQuvelBoot } from './boot/quvel.js';
47
48
  export { serviceContainerPlugin } from './stores/plugins/serviceContainer.js';
48
49
  export { defineQuvelModule } from './module.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAUH,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AAGjC,YAAY,EACV,OAAO,IAAI,QAAQ,EACnB,eAAe,EACf,eAAe,EACf,eAAe,EACf,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAGvD,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAG1D,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGtE,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAG7D,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AAGvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AAGtC,cAAc,gBAAgB,CAAC;AAG/B,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGlD,OAAO,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAG9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGtD,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAC9E,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAUH,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AAGjC,YAAY,EACV,OAAO,IAAI,QAAQ,EACnB,eAAe,EACf,eAAe,EACf,eAAe,EACf,eAAe,GAChB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAGvD,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAG1D,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGtE,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACnE,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAG7D,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AAGvC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AAGtC,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGvC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAGlD,OAAO,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAG9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAGtD,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAC9E,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC"}
package/dist/index.js CHANGED
@@ -55,6 +55,7 @@ export * from './utils/assets.js';
55
55
  export * from './utils/pagination.js';
56
56
  // ORM - Active Record pattern for models
57
57
  export * from './orm/index.js';
58
+ export { Model } from './orm/Model.js';
58
59
  // Boot
59
60
  export { defineQuvelBoot } from './boot/quvel.js';
60
61
  // Stores
@@ -0,0 +1,469 @@
1
+ import type { ServiceContainer, ApiService } from '@quvel-kit/core';
2
+ import type { ModelConfig, RelationshipDefinition } from '@quvel-kit/core/orm';
3
+ import type { QueryBuilder } from '@quvel-kit/core/orm';
4
+ /**
5
+ * Abstract Base Model Class
6
+ *
7
+ * Provides Active Record pattern for front-end models with:
8
+ * - Instance methods: save(), delete(), refresh(), isDirty()
9
+ * - Static query builder: User.query().where(...).get()
10
+ * - Relationship definitions: hasMany(), belongsTo(), hasOne()
11
+ * - Integration with ServiceContainer for API access
12
+ * - Full TypeScript type safety
13
+ * - SSR compatibility
14
+ *
15
+ * @template T - The model class itself (for proper return types)
16
+ * @template A - The API response interface (IUser, IPost, etc.)
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * export interface IUser {
21
+ * id: string;
22
+ * name: string;
23
+ * email: string;
24
+ * }
25
+ *
26
+ * export class User extends Model<User, IUser> implements IUser {
27
+ * static config: ModelConfig = {
28
+ * endpoint: '/api/v1/users',
29
+ * dates: ['created_at', 'updated_at']
30
+ * };
31
+ *
32
+ * id!: string;
33
+ * name!: string;
34
+ * email!: string;
35
+ *
36
+ * // Relationship definitions
37
+ * posts() {
38
+ * return this.hasMany(Post, 'user_id');
39
+ * }
40
+ * }
41
+ * ```
42
+ */
43
+ export declare abstract class Model<T extends Model<T, A>, A = any> {
44
+ /**
45
+ * Model configuration - MUST be overridden by subclasses
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * static config: ModelConfig = {
50
+ * endpoint: '/api/v1/users',
51
+ * primaryKey: 'id',
52
+ * dates: ['created_at', 'updated_at']
53
+ * };
54
+ * ```
55
+ */
56
+ static config: ModelConfig;
57
+ /**
58
+ * Global service container for all models
59
+ * Set once in boot file via Model.setContainer($quvel)
60
+ * @private
61
+ */
62
+ private static _globalContainer?;
63
+ /**
64
+ * Set global service container for all models
65
+ *
66
+ * Should be called once in your boot file to enable automatic container injection.
67
+ *
68
+ * @param container - ServiceContainer instance
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * // In boot file
73
+ * import { Model } from '@quvel-kit/core/orm';
74
+ *
75
+ * export default boot(({ app }) => {
76
+ * const container = app.config.globalProperties.$quvel;
77
+ * Model.setContainer(container);
78
+ * });
79
+ * ```
80
+ */
81
+ static setContainer(container: ServiceContainer): void;
82
+ /**
83
+ * Get the global service container
84
+ * @returns ServiceContainer or undefined if not set
85
+ */
86
+ static getContainer(): ServiceContainer | undefined;
87
+ /**
88
+ * Service container instance
89
+ * Provides access to ApiService and other services
90
+ */
91
+ protected $quvel: ServiceContainer;
92
+ /**
93
+ * Metadata for internal state tracking
94
+ * @private
95
+ */
96
+ private $meta;
97
+ /**
98
+ * Constructor - accepts raw API data and optional container
99
+ *
100
+ * @param attributes - Raw attributes from API response
101
+ * @param container - ServiceContainer instance (auto-injected in most cases)
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * // From API response
106
+ * const user = new User({ id: '1', name: 'John', email: 'john@example.com' });
107
+ *
108
+ * // With container (in stores)
109
+ * const user = new User(apiData, this.$quvel);
110
+ *
111
+ * // New unsaved record
112
+ * const user = new User({ name: 'Jane', email: 'jane@example.com' }, $quvel);
113
+ * await user.save(); // POST /api/v1/users
114
+ * ```
115
+ */
116
+ constructor(attributes?: Partial<A>, container?: ServiceContainer);
117
+ /**
118
+ * Fill model with attributes
119
+ * Handles property assignment and date parsing
120
+ *
121
+ * @param attributes - Attributes to assign to the model
122
+ */
123
+ protected fill(attributes: Partial<A> | Record<string, any>): void;
124
+ /**
125
+ * Get the API service instance
126
+ * @throws Error if model not initialized with ServiceContainer
127
+ */
128
+ protected get api(): ApiService;
129
+ /**
130
+ * Get model configuration
131
+ */
132
+ protected get config(): ModelConfig;
133
+ /**
134
+ * Get primary key value from object or current instance
135
+ *
136
+ * @param obj - Object to get primary key from (defaults to this)
137
+ * @returns Primary key value or undefined
138
+ */
139
+ protected getPrimaryKeyValue(obj?: any): any;
140
+ /**
141
+ * Get the resource URL for this model instance
142
+ * @example '/api/v1/users/123'
143
+ * @throws Error if model is unsaved (no primary key)
144
+ */
145
+ protected getResourceUrl(): string;
146
+ /**
147
+ * Create a new query builder for this model
148
+ *
149
+ * @param container - Optional ServiceContainer (uses global if available)
150
+ * @returns QueryBuilder instance for fluent API
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * const users = await User.query()
155
+ * .where('status', 'active')
156
+ * .with('posts', 'profile')
157
+ * .get();
158
+ * ```
159
+ */
160
+ static query<T extends Model<T, A>, A = any>(this: new (attrs: Partial<A>) => T): QueryBuilder<T, A>;
161
+ /**
162
+ * Find a model by primary key
163
+ *
164
+ * @param id - Primary key value
165
+ * @param container - Optional ServiceContainer
166
+ * @returns Model instance or null if not found
167
+ *
168
+ * @example
169
+ * ```typescript
170
+ * const user = await User.find('123');
171
+ * if (user) {
172
+ * console.log(user.name);
173
+ * }
174
+ * ```
175
+ */
176
+ static find<T extends Model<T, A>, A = any>(this: new (attrs: Partial<A>, container?: ServiceContainer) => T, id: string | number, container?: ServiceContainer): Promise<T | null>;
177
+ /**
178
+ * Find a model by primary key or throw an error
179
+ *
180
+ * @param id - Primary key value
181
+ * @param container - Optional ServiceContainer
182
+ * @returns Model instance
183
+ * @throws Error if not found
184
+ *
185
+ * @example
186
+ * ```typescript
187
+ * const user = await User.findOrFail('123');
188
+ * // Throws if user doesn't exist
189
+ * ```
190
+ */
191
+ static findOrFail<T extends Model<T, A>, A = any>(this: new (attrs: Partial<A>, container?: ServiceContainer) => T, id: string | number, container?: ServiceContainer): Promise<T>;
192
+ /**
193
+ * Save the model (create or update)
194
+ *
195
+ * - New models (no primary key): POST to endpoint
196
+ * - Existing models: PUT to resource URL
197
+ *
198
+ * @returns The saved model instance
199
+ * @throws Error if already saving or if API request fails
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * // Create new
204
+ * const user = new User({ name: 'John', email: 'john@example.com' }, $quvel);
205
+ * await user.save(); // POST /api/v1/users
206
+ *
207
+ * // Update existing
208
+ * user.name = 'Jane';
209
+ * await user.save(); // PUT /api/v1/users/123
210
+ * ```
211
+ */
212
+ save(): Promise<T>;
213
+ /**
214
+ * Delete the model from the server
215
+ *
216
+ * @throws Error if model is unsaved or already being deleted
217
+ *
218
+ * @example
219
+ * ```typescript
220
+ * await user.delete(); // DELETE /api/v1/users/123
221
+ * ```
222
+ */
223
+ delete(): Promise<void>;
224
+ /**
225
+ * Refresh the model from the API
226
+ * Re-fetches the current state from the server
227
+ *
228
+ * @returns The refreshed model instance
229
+ * @throws Error if model is unsaved
230
+ *
231
+ * @example
232
+ * ```typescript
233
+ * await user.refresh(); // GET /api/v1/users/123
234
+ * console.log(user.name); // Updated from server
235
+ * ```
236
+ */
237
+ refresh(): Promise<T>;
238
+ /**
239
+ * Check if model has unsaved changes (dirty checking)
240
+ *
241
+ * Compares current attributes with original attributes from API
242
+ *
243
+ * @returns true if there are unsaved changes
244
+ *
245
+ * @example
246
+ * ```typescript
247
+ * const user = await User.find('123');
248
+ * user.name = 'Changed';
249
+ * console.log(user.isDirty()); // true
250
+ * await user.save();
251
+ * console.log(user.isDirty()); // false
252
+ * ```
253
+ */
254
+ isDirty(): boolean;
255
+ /**
256
+ * Check if a specific attribute has changed
257
+ *
258
+ * @param key - Attribute name to check
259
+ * @returns true if the attribute has changed
260
+ *
261
+ * @example
262
+ * ```typescript
263
+ * user.name = 'New Name';
264
+ * console.log(user.isDirtyAttribute('name')); // true
265
+ * console.log(user.isDirtyAttribute('email')); // false
266
+ * ```
267
+ */
268
+ isDirtyAttribute(key: keyof A): boolean;
269
+ /**
270
+ * Get model attributes as plain object
271
+ * Excludes metadata, methods, and internal properties
272
+ *
273
+ * @returns Plain object of model attributes
274
+ *
275
+ * @example
276
+ * ```typescript
277
+ * const attrs = user.getAttributes();
278
+ * console.log(attrs); // { id: '123', name: 'John', email: 'john@example.com' }
279
+ * ```
280
+ */
281
+ getAttributes(): Record<string, any>;
282
+ /**
283
+ * Get original attribute value (before changes)
284
+ *
285
+ * @param key - Attribute name
286
+ * @returns Original value from API
287
+ *
288
+ * @example
289
+ * ```typescript
290
+ * const user = await User.find('123');
291
+ * console.log(user.name); // 'John'
292
+ * user.name = 'Jane';
293
+ * console.log(user.getOriginal('name')); // 'John'
294
+ * ```
295
+ */
296
+ getOriginal<K extends keyof A>(key: K): A[K] | undefined;
297
+ /**
298
+ * Reset model to original state (undo changes)
299
+ *
300
+ * @example
301
+ * ```typescript
302
+ * user.name = 'Changed';
303
+ * user.reset();
304
+ * console.log(user.name); // Back to original value
305
+ * ```
306
+ */
307
+ reset(): void;
308
+ /**
309
+ * Get a loaded relationship
310
+ *
311
+ * @param name - Relationship name (method name on model)
312
+ * @returns Related model(s) or undefined if not loaded
313
+ *
314
+ * @example
315
+ * ```typescript
316
+ * const users = await User.query().with('posts').get();
317
+ * const posts = users[0].getRelation<Post[]>('posts');
318
+ * ```
319
+ */
320
+ getRelation<R = any>(name: string): R | undefined;
321
+ /**
322
+ * Set a loaded relationship
323
+ * Used internally by QueryBuilder when hydrating relationships
324
+ *
325
+ * @param name - Relationship name
326
+ * @param value - Related model(s)
327
+ */
328
+ setRelation<R = any>(name: string, value: R): void;
329
+ /**
330
+ * Check if a relationship is loaded
331
+ *
332
+ * @param name - Relationship name
333
+ * @returns true if relationship is loaded
334
+ *
335
+ * @example
336
+ * ```typescript
337
+ * if (user.hasRelation('posts')) {
338
+ * const posts = user.getRelation<Post[]>('posts');
339
+ * }
340
+ * ```
341
+ */
342
+ hasRelation(name: string): boolean;
343
+ /**
344
+ * Define a hasMany relationship
345
+ *
346
+ * **Note**: This only defines the relationship metadata.
347
+ * Actual loading must be done via query builder's `with()` method.
348
+ *
349
+ * @param related - Related model class
350
+ * @param foreignKey - Foreign key on related model
351
+ * @param localKey - Local key on this model (default: primary key)
352
+ * @returns Relationship definition
353
+ *
354
+ * @example
355
+ * ```typescript
356
+ * export class User extends Model<User, IUser> {
357
+ * posts() {
358
+ * return this.hasMany(Post, 'user_id');
359
+ * }
360
+ * }
361
+ *
362
+ * // Usage
363
+ * const users = await User.query().with('posts').get();
364
+ * const posts = users[0].getRelation<Post[]>('posts');
365
+ * ```
366
+ */
367
+ protected hasMany<R extends Model<R, any>>(related: new (...args: any[]) => R, foreignKey: string, localKey?: string): RelationshipDefinition<R[]>;
368
+ /**
369
+ * Define a belongsTo relationship
370
+ *
371
+ * **Note**: This only defines the relationship metadata.
372
+ * Actual loading must be done via query builder's `with()` method.
373
+ *
374
+ * @param related - Related model class
375
+ * @param foreignKey - Foreign key on this model
376
+ * @param ownerKey - Owner key on related model (default: 'id')
377
+ * @returns Relationship definition
378
+ *
379
+ * @example
380
+ * ```typescript
381
+ * export class Post extends Model<Post, IPost> {
382
+ * user() {
383
+ * return this.belongsTo(User, 'user_id');
384
+ * }
385
+ * }
386
+ *
387
+ * // Usage
388
+ * const posts = await Post.query().with('user').get();
389
+ * const user = posts[0].getRelation<User>('user');
390
+ * ```
391
+ */
392
+ protected belongsTo<R extends Model<R, any>>(related: new (...args: any[]) => R, foreignKey: string, ownerKey?: string): RelationshipDefinition<R | null>;
393
+ /**
394
+ * Define a hasOne relationship
395
+ *
396
+ * **Note**: This only defines the relationship metadata.
397
+ * Actual loading must be done via query builder's `with()` method.
398
+ *
399
+ * @param related - Related model class
400
+ * @param foreignKey - Foreign key on related model
401
+ * @param localKey - Local key on this model (default: primary key)
402
+ * @returns Relationship definition
403
+ *
404
+ * @example
405
+ * ```typescript
406
+ * export class User extends Model<User, IUser> {
407
+ * profile() {
408
+ * return this.hasOne(Profile, 'user_id');
409
+ * }
410
+ * }
411
+ *
412
+ * // Usage
413
+ * const users = await User.query().with('profile').get();
414
+ * const profile = users[0].getRelation<Profile>('profile');
415
+ * ```
416
+ */
417
+ protected hasOne<R extends Model<R, any>>(related: new (...args: any[]) => R, foreignKey: string, localKey?: string): RelationshipDefinition<R | null>;
418
+ /**
419
+ * Factory method - creates instance from API data
420
+ * Provides backward compatibility with existing `fromApi` pattern
421
+ *
422
+ * @param data - API response data
423
+ * @param container - Optional ServiceContainer
424
+ * @returns Model instance
425
+ *
426
+ * @example
427
+ * ```typescript
428
+ * // Old pattern (still works)
429
+ * const user = User.fromApi(apiData);
430
+ *
431
+ * // New pattern
432
+ * const user = new User(apiData, $quvel);
433
+ * ```
434
+ */
435
+ static fromApi<T extends Model<T, A>, A = any>(this: new (attrs: Partial<A>, container?: ServiceContainer) => T, data: A, container?: ServiceContainer): T;
436
+ /**
437
+ * Serialize model to JSON
438
+ * Useful for API responses, logging, etc.
439
+ *
440
+ * @returns Plain object representation
441
+ *
442
+ * @example
443
+ * ```typescript
444
+ * console.log(JSON.stringify(user.toJSON()));
445
+ * ```
446
+ */
447
+ toJSON(): Record<string, any>;
448
+ /**
449
+ * Check if this is a new (unsaved) model
450
+ *
451
+ * @returns true if model has not been saved to the server
452
+ *
453
+ * @example
454
+ * ```typescript
455
+ * const user = new User({ name: 'John' });
456
+ * console.log(user.isNew()); // true
457
+ * await user.save();
458
+ * console.log(user.isNew()); // false
459
+ * ```
460
+ */
461
+ isNew(): boolean;
462
+ /**
463
+ * Check if this model exists on the server
464
+ *
465
+ * @returns true if model has been saved
466
+ */
467
+ exists(): boolean;
468
+ }
469
+ //# sourceMappingURL=Model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Model.d.ts","sourceRoot":"","sources":["../../src/orm/Model.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,648 @@
1
+ /**
2
+ * Abstract Base Model Class
3
+ *
4
+ * Provides Active Record pattern for front-end models with:
5
+ * - Instance methods: save(), delete(), refresh(), isDirty()
6
+ * - Static query builder: User.query().where(...).get()
7
+ * - Relationship definitions: hasMany(), belongsTo(), hasOne()
8
+ * - Integration with ServiceContainer for API access
9
+ * - Full TypeScript type safety
10
+ * - SSR compatibility
11
+ *
12
+ * @template T - The model class itself (for proper return types)
13
+ * @template A - The API response interface (IUser, IPost, etc.)
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * export interface IUser {
18
+ * id: string;
19
+ * name: string;
20
+ * email: string;
21
+ * }
22
+ *
23
+ * export class User extends Model<User, IUser> implements IUser {
24
+ * static config: ModelConfig = {
25
+ * endpoint: '/api/v1/users',
26
+ * dates: ['created_at', 'updated_at']
27
+ * };
28
+ *
29
+ * id!: string;
30
+ * name!: string;
31
+ * email!: string;
32
+ *
33
+ * // Relationship definitions
34
+ * posts() {
35
+ * return this.hasMany(Post, 'user_id');
36
+ * }
37
+ * }
38
+ * ```
39
+ */
40
+ export class Model {
41
+ /**
42
+ * Model configuration - MUST be overridden by subclasses
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * static config: ModelConfig = {
47
+ * endpoint: '/api/v1/users',
48
+ * primaryKey: 'id',
49
+ * dates: ['created_at', 'updated_at']
50
+ * };
51
+ * ```
52
+ */
53
+ static config;
54
+ /**
55
+ * Global service container for all models
56
+ * Set once in boot file via Model.setContainer($quvel)
57
+ * @private
58
+ */
59
+ static _globalContainer;
60
+ /**
61
+ * Set global service container for all models
62
+ *
63
+ * Should be called once in your boot file to enable automatic container injection.
64
+ *
65
+ * @param container - ServiceContainer instance
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * // In boot file
70
+ * import { Model } from '@quvel-kit/core/orm';
71
+ *
72
+ * export default boot(({ app }) => {
73
+ * const container = app.config.globalProperties.$quvel;
74
+ * Model.setContainer(container);
75
+ * });
76
+ * ```
77
+ */
78
+ static setContainer(container) {
79
+ Model._globalContainer = container;
80
+ }
81
+ /**
82
+ * Get the global service container
83
+ * @returns ServiceContainer or undefined if not set
84
+ */
85
+ static getContainer() {
86
+ return Model._globalContainer;
87
+ }
88
+ /**
89
+ * Service container instance
90
+ * Provides access to ApiService and other services
91
+ */
92
+ $quvel;
93
+ /**
94
+ * Metadata for internal state tracking
95
+ * @private
96
+ */
97
+ $meta;
98
+ /**
99
+ * Constructor - accepts raw API data and optional container
100
+ *
101
+ * @param attributes - Raw attributes from API response
102
+ * @param container - ServiceContainer instance (auto-injected in most cases)
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * // From API response
107
+ * const user = new User({ id: '1', name: 'John', email: 'john@example.com' });
108
+ *
109
+ * // With container (in stores)
110
+ * const user = new User(apiData, this.$quvel);
111
+ *
112
+ * // New unsaved record
113
+ * const user = new User({ name: 'Jane', email: 'jane@example.com' }, $quvel);
114
+ * await user.save(); // POST /api/v1/users
115
+ * ```
116
+ */
117
+ constructor(attributes = {}, container) {
118
+ // Initialize metadata
119
+ this.$meta = {
120
+ isNew: !this.getPrimaryKeyValue(attributes),
121
+ original: { ...attributes },
122
+ relations: new Map(),
123
+ isSaving: false,
124
+ isDeleting: false,
125
+ };
126
+ // Assign attributes to model
127
+ this.fill(attributes);
128
+ // Container will be injected later if not provided
129
+ if (container) {
130
+ this.$quvel = container;
131
+ }
132
+ }
133
+ /**
134
+ * Fill model with attributes
135
+ * Handles property assignment and date parsing
136
+ *
137
+ * @param attributes - Attributes to assign to the model
138
+ */
139
+ fill(attributes) {
140
+ Object.assign(this, attributes);
141
+ // Parse date fields
142
+ const config = this.constructor.config;
143
+ if (config.dates) {
144
+ for (const field of config.dates) {
145
+ if (field in attributes && typeof attributes[field] === 'string') {
146
+ this[field] = new Date(attributes[field]);
147
+ }
148
+ }
149
+ }
150
+ }
151
+ /**
152
+ * Get the API service instance
153
+ * @throws Error if model not initialized with ServiceContainer
154
+ */
155
+ get api() {
156
+ if (!this.$quvel) {
157
+ throw new Error(`${this.constructor.name} not initialized with ServiceContainer. ` +
158
+ `Pass container to constructor: new ${this.constructor.name}(data, container)`);
159
+ }
160
+ return this.$quvel.api;
161
+ }
162
+ /**
163
+ * Get model configuration
164
+ */
165
+ get config() {
166
+ return this.constructor.config;
167
+ }
168
+ /**
169
+ * Get primary key value from object or current instance
170
+ *
171
+ * @param obj - Object to get primary key from (defaults to this)
172
+ * @returns Primary key value or undefined
173
+ */
174
+ getPrimaryKeyValue(obj) {
175
+ const target = obj ?? this;
176
+ const key = this.config.primaryKey ?? 'id';
177
+ return target[key];
178
+ }
179
+ /**
180
+ * Get the resource URL for this model instance
181
+ * @example '/api/v1/users/123'
182
+ * @throws Error if model is unsaved (no primary key)
183
+ */
184
+ getResourceUrl() {
185
+ const pk = this.getPrimaryKeyValue();
186
+ if (!pk) {
187
+ throw new Error(`Cannot get resource URL for unsaved ${this.constructor.name}`);
188
+ }
189
+ return `${this.config.endpoint}/${pk}`;
190
+ }
191
+ /**
192
+ * Create a new query builder for this model
193
+ *
194
+ * @param container - Optional ServiceContainer (uses global if available)
195
+ * @returns QueryBuilder instance for fluent API
196
+ *
197
+ * @example
198
+ * ```typescript
199
+ * const users = await User.query()
200
+ * .where('status', 'active')
201
+ * .with('posts', 'profile')
202
+ * .get();
203
+ * ```
204
+ */
205
+ static query() {
206
+ // Dynamic import to avoid circular dependency
207
+ const { QueryBuilder } = require('@quvel-kit/core/orm');
208
+ return new QueryBuilder(this, Model._globalContainer);
209
+ }
210
+ /**
211
+ * Find a model by primary key
212
+ *
213
+ * @param id - Primary key value
214
+ * @param container - Optional ServiceContainer
215
+ * @returns Model instance or null if not found
216
+ *
217
+ * @example
218
+ * ```typescript
219
+ * const user = await User.find('123');
220
+ * if (user) {
221
+ * console.log(user.name);
222
+ * }
223
+ * ```
224
+ */
225
+ static async find(id, container) {
226
+ const pkField = this.config.primaryKey ?? 'id';
227
+ return this.query(container)
228
+ .where(pkField, id)
229
+ .first();
230
+ }
231
+ /**
232
+ * Find a model by primary key or throw an error
233
+ *
234
+ * @param id - Primary key value
235
+ * @param container - Optional ServiceContainer
236
+ * @returns Model instance
237
+ * @throws Error if not found
238
+ *
239
+ * @example
240
+ * ```typescript
241
+ * const user = await User.findOrFail('123');
242
+ * // Throws if user doesn't exist
243
+ * ```
244
+ */
245
+ static async findOrFail(id, container) {
246
+ const model = await this.find(id, container);
247
+ if (!model) {
248
+ throw new Error(`${this.name} with id ${id} not found`);
249
+ }
250
+ return model;
251
+ }
252
+ /**
253
+ * Save the model (create or update)
254
+ *
255
+ * - New models (no primary key): POST to endpoint
256
+ * - Existing models: PUT to resource URL
257
+ *
258
+ * @returns The saved model instance
259
+ * @throws Error if already saving or if API request fails
260
+ *
261
+ * @example
262
+ * ```typescript
263
+ * // Create new
264
+ * const user = new User({ name: 'John', email: 'john@example.com' }, $quvel);
265
+ * await user.save(); // POST /api/v1/users
266
+ *
267
+ * // Update existing
268
+ * user.name = 'Jane';
269
+ * await user.save(); // PUT /api/v1/users/123
270
+ * ```
271
+ */
272
+ async save() {
273
+ if (this.$meta.isSaving) {
274
+ throw new Error(`${this.constructor.name} is already being saved`);
275
+ }
276
+ this.$meta.isSaving = true;
277
+ try {
278
+ const attributes = this.getAttributes();
279
+ if (this.$meta.isNew) {
280
+ // Create new record via POST
281
+ const response = await this.api.post(this.config.endpoint, attributes);
282
+ const data = (response && typeof response === 'object' && 'data' in response)
283
+ ? response.data
284
+ : response;
285
+ this.fill(data);
286
+ this.$meta.isNew = false;
287
+ this.$meta.original = { ...data };
288
+ }
289
+ else {
290
+ // Update existing record via PUT
291
+ const response = await this.api.put(this.getResourceUrl(), attributes);
292
+ const data = (response && typeof response === 'object' && 'data' in response)
293
+ ? response.data
294
+ : response;
295
+ this.fill(data);
296
+ this.$meta.original = { ...data };
297
+ }
298
+ return this;
299
+ }
300
+ finally {
301
+ this.$meta.isSaving = false;
302
+ }
303
+ }
304
+ /**
305
+ * Delete the model from the server
306
+ *
307
+ * @throws Error if model is unsaved or already being deleted
308
+ *
309
+ * @example
310
+ * ```typescript
311
+ * await user.delete(); // DELETE /api/v1/users/123
312
+ * ```
313
+ */
314
+ async delete() {
315
+ if (this.$meta.isNew) {
316
+ throw new Error(`Cannot delete unsaved ${this.constructor.name}`);
317
+ }
318
+ if (this.$meta.isDeleting) {
319
+ throw new Error(`${this.constructor.name} is already being deleted`);
320
+ }
321
+ this.$meta.isDeleting = true;
322
+ try {
323
+ await this.api.delete(this.getResourceUrl());
324
+ }
325
+ finally {
326
+ this.$meta.isDeleting = false;
327
+ }
328
+ }
329
+ /**
330
+ * Refresh the model from the API
331
+ * Re-fetches the current state from the server
332
+ *
333
+ * @returns The refreshed model instance
334
+ * @throws Error if model is unsaved
335
+ *
336
+ * @example
337
+ * ```typescript
338
+ * await user.refresh(); // GET /api/v1/users/123
339
+ * console.log(user.name); // Updated from server
340
+ * ```
341
+ */
342
+ async refresh() {
343
+ if (this.$meta.isNew) {
344
+ throw new Error(`Cannot refresh unsaved ${this.constructor.name}`);
345
+ }
346
+ const response = await this.api.get(this.getResourceUrl());
347
+ const data = (response && typeof response === 'object' && 'data' in response)
348
+ ? response.data
349
+ : response;
350
+ this.fill(data);
351
+ this.$meta.original = { ...data };
352
+ return this;
353
+ }
354
+ /**
355
+ * Check if model has unsaved changes (dirty checking)
356
+ *
357
+ * Compares current attributes with original attributes from API
358
+ *
359
+ * @returns true if there are unsaved changes
360
+ *
361
+ * @example
362
+ * ```typescript
363
+ * const user = await User.find('123');
364
+ * user.name = 'Changed';
365
+ * console.log(user.isDirty()); // true
366
+ * await user.save();
367
+ * console.log(user.isDirty()); // false
368
+ * ```
369
+ */
370
+ isDirty() {
371
+ const current = this.getAttributes();
372
+ const original = this.$meta.original;
373
+ for (const key in current) {
374
+ if (current[key] !== original[key]) {
375
+ return true;
376
+ }
377
+ }
378
+ return false;
379
+ }
380
+ /**
381
+ * Check if a specific attribute has changed
382
+ *
383
+ * @param key - Attribute name to check
384
+ * @returns true if the attribute has changed
385
+ *
386
+ * @example
387
+ * ```typescript
388
+ * user.name = 'New Name';
389
+ * console.log(user.isDirtyAttribute('name')); // true
390
+ * console.log(user.isDirtyAttribute('email')); // false
391
+ * ```
392
+ */
393
+ isDirtyAttribute(key) {
394
+ const current = this.getAttributes();
395
+ const original = this.$meta.original;
396
+ return current[key] !== original[key];
397
+ }
398
+ /**
399
+ * Get model attributes as plain object
400
+ * Excludes metadata, methods, and internal properties
401
+ *
402
+ * @returns Plain object of model attributes
403
+ *
404
+ * @example
405
+ * ```typescript
406
+ * const attrs = user.getAttributes();
407
+ * console.log(attrs); // { id: '123', name: 'John', email: 'john@example.com' }
408
+ * ```
409
+ */
410
+ getAttributes() {
411
+ const attrs = {};
412
+ for (const key in this) {
413
+ // Skip internal properties, methods, and container
414
+ if (key !== '$quvel' &&
415
+ key !== '$meta' &&
416
+ typeof this[key] !== 'function' &&
417
+ !key.startsWith('$')) {
418
+ attrs[key] = this[key];
419
+ }
420
+ }
421
+ return attrs;
422
+ }
423
+ /**
424
+ * Get original attribute value (before changes)
425
+ *
426
+ * @param key - Attribute name
427
+ * @returns Original value from API
428
+ *
429
+ * @example
430
+ * ```typescript
431
+ * const user = await User.find('123');
432
+ * console.log(user.name); // 'John'
433
+ * user.name = 'Jane';
434
+ * console.log(user.getOriginal('name')); // 'John'
435
+ * ```
436
+ */
437
+ getOriginal(key) {
438
+ return this.$meta.original[key];
439
+ }
440
+ /**
441
+ * Reset model to original state (undo changes)
442
+ *
443
+ * @example
444
+ * ```typescript
445
+ * user.name = 'Changed';
446
+ * user.reset();
447
+ * console.log(user.name); // Back to original value
448
+ * ```
449
+ */
450
+ reset() {
451
+ this.fill(this.$meta.original);
452
+ }
453
+ /**
454
+ * Get a loaded relationship
455
+ *
456
+ * @param name - Relationship name (method name on model)
457
+ * @returns Related model(s) or undefined if not loaded
458
+ *
459
+ * @example
460
+ * ```typescript
461
+ * const users = await User.query().with('posts').get();
462
+ * const posts = users[0].getRelation<Post[]>('posts');
463
+ * ```
464
+ */
465
+ getRelation(name) {
466
+ return this.$meta.relations.get(name);
467
+ }
468
+ /**
469
+ * Set a loaded relationship
470
+ * Used internally by QueryBuilder when hydrating relationships
471
+ *
472
+ * @param name - Relationship name
473
+ * @param value - Related model(s)
474
+ */
475
+ setRelation(name, value) {
476
+ this.$meta.relations.set(name, value);
477
+ }
478
+ /**
479
+ * Check if a relationship is loaded
480
+ *
481
+ * @param name - Relationship name
482
+ * @returns true if relationship is loaded
483
+ *
484
+ * @example
485
+ * ```typescript
486
+ * if (user.hasRelation('posts')) {
487
+ * const posts = user.getRelation<Post[]>('posts');
488
+ * }
489
+ * ```
490
+ */
491
+ hasRelation(name) {
492
+ return this.$meta.relations.has(name);
493
+ }
494
+ /**
495
+ * Define a hasMany relationship
496
+ *
497
+ * **Note**: This only defines the relationship metadata.
498
+ * Actual loading must be done via query builder's `with()` method.
499
+ *
500
+ * @param related - Related model class
501
+ * @param foreignKey - Foreign key on related model
502
+ * @param localKey - Local key on this model (default: primary key)
503
+ * @returns Relationship definition
504
+ *
505
+ * @example
506
+ * ```typescript
507
+ * export class User extends Model<User, IUser> {
508
+ * posts() {
509
+ * return this.hasMany(Post, 'user_id');
510
+ * }
511
+ * }
512
+ *
513
+ * // Usage
514
+ * const users = await User.query().with('posts').get();
515
+ * const posts = users[0].getRelation<Post[]>('posts');
516
+ * ```
517
+ */
518
+ hasMany(related, foreignKey, localKey) {
519
+ return {
520
+ type: 'hasMany',
521
+ related,
522
+ foreignKey,
523
+ localKey: localKey ?? (this.config.primaryKey ?? 'id'),
524
+ };
525
+ }
526
+ /**
527
+ * Define a belongsTo relationship
528
+ *
529
+ * **Note**: This only defines the relationship metadata.
530
+ * Actual loading must be done via query builder's `with()` method.
531
+ *
532
+ * @param related - Related model class
533
+ * @param foreignKey - Foreign key on this model
534
+ * @param ownerKey - Owner key on related model (default: 'id')
535
+ * @returns Relationship definition
536
+ *
537
+ * @example
538
+ * ```typescript
539
+ * export class Post extends Model<Post, IPost> {
540
+ * user() {
541
+ * return this.belongsTo(User, 'user_id');
542
+ * }
543
+ * }
544
+ *
545
+ * // Usage
546
+ * const posts = await Post.query().with('user').get();
547
+ * const user = posts[0].getRelation<User>('user');
548
+ * ```
549
+ */
550
+ belongsTo(related, foreignKey, ownerKey) {
551
+ return {
552
+ type: 'belongsTo',
553
+ related,
554
+ foreignKey,
555
+ ownerKey: ownerKey ?? 'id',
556
+ };
557
+ }
558
+ /**
559
+ * Define a hasOne relationship
560
+ *
561
+ * **Note**: This only defines the relationship metadata.
562
+ * Actual loading must be done via query builder's `with()` method.
563
+ *
564
+ * @param related - Related model class
565
+ * @param foreignKey - Foreign key on related model
566
+ * @param localKey - Local key on this model (default: primary key)
567
+ * @returns Relationship definition
568
+ *
569
+ * @example
570
+ * ```typescript
571
+ * export class User extends Model<User, IUser> {
572
+ * profile() {
573
+ * return this.hasOne(Profile, 'user_id');
574
+ * }
575
+ * }
576
+ *
577
+ * // Usage
578
+ * const users = await User.query().with('profile').get();
579
+ * const profile = users[0].getRelation<Profile>('profile');
580
+ * ```
581
+ */
582
+ hasOne(related, foreignKey, localKey) {
583
+ return {
584
+ type: 'hasOne',
585
+ related,
586
+ foreignKey,
587
+ localKey: localKey ?? (this.config.primaryKey ?? 'id'),
588
+ };
589
+ }
590
+ /**
591
+ * Factory method - creates instance from API data
592
+ * Provides backward compatibility with existing `fromApi` pattern
593
+ *
594
+ * @param data - API response data
595
+ * @param container - Optional ServiceContainer
596
+ * @returns Model instance
597
+ *
598
+ * @example
599
+ * ```typescript
600
+ * // Old pattern (still works)
601
+ * const user = User.fromApi(apiData);
602
+ *
603
+ * // New pattern
604
+ * const user = new User(apiData, $quvel);
605
+ * ```
606
+ */
607
+ static fromApi(data, container) {
608
+ return new this(data, container);
609
+ }
610
+ /**
611
+ * Serialize model to JSON
612
+ * Useful for API responses, logging, etc.
613
+ *
614
+ * @returns Plain object representation
615
+ *
616
+ * @example
617
+ * ```typescript
618
+ * console.log(JSON.stringify(user.toJSON()));
619
+ * ```
620
+ */
621
+ toJSON() {
622
+ return this.getAttributes();
623
+ }
624
+ /**
625
+ * Check if this is a new (unsaved) model
626
+ *
627
+ * @returns true if model has not been saved to the server
628
+ *
629
+ * @example
630
+ * ```typescript
631
+ * const user = new User({ name: 'John' });
632
+ * console.log(user.isNew()); // true
633
+ * await user.save();
634
+ * console.log(user.isNew()); // false
635
+ * ```
636
+ */
637
+ isNew() {
638
+ return this.$meta.isNew;
639
+ }
640
+ /**
641
+ * Check if this model exists on the server
642
+ *
643
+ * @returns true if model has been saved
644
+ */
645
+ exists() {
646
+ return !this.$meta.isNew;
647
+ }
648
+ }
@@ -1,19 +1,27 @@
1
1
  /**
2
2
  * ORM Module - Front-End Active Record Pattern for Spatie Query Builder
3
3
  *
4
- * Provides QueryBuilder and types for building Active Record models.
5
- * Apps should create their own base Model class - see app/src/models/BaseModel.ts
4
+ * Provides complete Active Record implementation with QueryBuilder.
5
+ * Advanced users can create intermediate base models or copy Model code into their project.
6
6
  *
7
7
  * @example
8
8
  * ```typescript
9
- * // In app/src/models/BaseModel.ts
10
- * import { QueryBuilder, type ModelConfig } from '@quvel-kit/core/orm';
9
+ * import { Model, type ModelConfig } from '@quvel-kit/core/orm';
11
10
  *
12
- * export abstract class Model<T, A> {
13
- * // Your customizable base model
11
+ * // Direct usage (recommended for most projects)
12
+ * export class User extends Model<User, IUser> implements IUser {
13
+ * static override config: ModelConfig = { endpoint: '/api/v1/users' };
14
+ * id!: string;
15
+ * name!: string;
16
+ * }
17
+ *
18
+ * // Optional: Intermediate base for app-wide customization
19
+ * export abstract class AppModel<T, A> extends Model<T, A> {
20
+ * myCustomMethod() { /* ... *\/ }
14
21
  * }
15
22
  * ```
16
23
  */
24
+ export { Model } from './Model.js';
17
25
  export { QueryBuilder } from './QueryBuilder.js';
18
26
  export type { ModelConfig, FilterOperator, ModelMetadata } from './types.js';
19
27
  export { type RelationshipType, type RelationshipDefinition, type RelationshipData, } from './relationships/types.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/orm/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG7E,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,GACtB,MAAM,0BAA0B,CAAC;AAGlC,YAAY,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/orm/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG7E,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,GACtB,MAAM,0BAA0B,CAAC;AAGlC,YAAY,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC"}
package/dist/orm/index.js CHANGED
@@ -1,18 +1,26 @@
1
1
  /**
2
2
  * ORM Module - Front-End Active Record Pattern for Spatie Query Builder
3
3
  *
4
- * Provides QueryBuilder and types for building Active Record models.
5
- * Apps should create their own base Model class - see app/src/models/BaseModel.ts
4
+ * Provides complete Active Record implementation with QueryBuilder.
5
+ * Advanced users can create intermediate base models or copy Model code into their project.
6
6
  *
7
7
  * @example
8
8
  * ```typescript
9
- * // In app/src/models/BaseModel.ts
10
- * import { QueryBuilder, type ModelConfig } from '@quvel-kit/core/orm';
9
+ * import { Model, type ModelConfig } from '@quvel-kit/core/orm';
11
10
  *
12
- * export abstract class Model<T, A> {
13
- * // Your customizable base model
11
+ * // Direct usage (recommended for most projects)
12
+ * export class User extends Model<User, IUser> implements IUser {
13
+ * static override config: ModelConfig = { endpoint: '/api/v1/users' };
14
+ * id!: string;
15
+ * name!: string;
16
+ * }
17
+ *
18
+ * // Optional: Intermediate base for app-wide customization
19
+ * export abstract class AppModel<T, A> extends Model<T, A> {
20
+ * myCustomMethod() { /* ... *\/ }
14
21
  * }
15
22
  * ```
16
23
  */
17
24
  // Core classes
25
+ export { Model } from './Model.js';
18
26
  export { QueryBuilder } from './QueryBuilder.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quvel-kit/core",
3
- "version": "1.3.21",
3
+ "version": "1.3.22",
4
4
  "description": "Core utilities for Quvel UI",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",