unconventional 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,799 @@
1
- # unconventional
2
- A complete backend server built on Cloudflare Workers/Pages and hono.
3
- Includes
4
- - Routing
5
- - Extensible CRUD methods
6
- - Database connection and operations
7
- - Middleware
8
- - Caching
9
- - CORS
10
- - Private field masking and IDOR protection
11
- - Error handling
12
-
13
- Out of the box!
14
-
15
- DOCS COMING SOON!
1
+ # Unconventional
2
+
3
+ > A complete backend framework built on Cloudflare Workers/Pages and Hono
4
+
5
+ [![npm version](https://img.shields.io/npm/v/unconventional)](https://www.npmjs.com/package/unconventional)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ **Unconventional** is a powerful, production-ready backend framework designed for Cloudflare Workers and Pages. It provides a complete set of tools for building RESTful APIs with minimal boilerplate, including automatic CRUD operations, database management, caching, security features, and more.
9
+
10
+ ## ✨ Features
11
+
12
+ - 🚀 **Complete CRUD Operations** - Automatic REST endpoints for all your models
13
+ - 🗄️ **PostgreSQL Integration** - Built-in support via `unconventional-pg-queries`
14
+ - 🎨 **Decorator-Based Models** - Define models with `@prop`, `@index`, and `@timestamp` decorators
15
+ - ⏰ **Automatic Timestamps** - Built-in `createdAt` and `updatedAt` management
16
+ - 💾 **Cloudflare KV Caching** - Intelligent caching layer with automatic invalidation
17
+ - 🔒 **Security First** - IDOR protection and private field masking out of the box
18
+ - 🔗 **Relationship Support** - HasOne, HasMany, BelongsTo, and ManyToMany relations
19
+ - 🛡️ **Middleware Support** - Extensible middleware system for authentication and more
20
+ - 📊 **Advanced Querying** - Pagination, filtering, sorting, and query expansion
21
+ - 🆔 **Flexible IDs** - Support for both numeric IDs and UUIDs
22
+ - 🌐 **CORS Ready** - Pre-configured CORS with sensible defaults
23
+ - ⚡ **Edge Optimized** - Built for Cloudflare's edge network
24
+
25
+ ## 📦 Installation
26
+
27
+ ```bash
28
+ npm install unconventional
29
+ ```
30
+
31
+ ### Peer Dependencies
32
+
33
+ Unconventional requires the following peer dependencies:
34
+
35
+ ```bash
36
+ npm install hono@^4 unconventional-pg-queries@^1.6.0
37
+ ```
38
+
39
+ **Note:** TypeScript is required for this package.
40
+
41
+ ## 🚀 Quick Start
42
+
43
+ ### 1. Define a Model
44
+
45
+ ```typescript
46
+ import { BaseModel, prop, timestamp } from 'unconventional';
47
+
48
+ @timestamp()
49
+ export class User extends BaseModel {
50
+ public static collection = 'users';
51
+ public static db: DB;
52
+
53
+ @prop({ required: true, unique: true })
54
+ public email!: string;
55
+
56
+ @prop({ required: true })
57
+ public name!: string;
58
+
59
+ @prop({ private: true })
60
+ public passwordHash?: string;
61
+ }
62
+ ```
63
+
64
+ ### 2. Create a Controller
65
+
66
+ ```typescript
67
+ import { AbstractBaseController } from 'unconventional';
68
+ import { User } from './models/user';
69
+
70
+ export class UserController extends AbstractBaseController<typeof User> {
71
+ constructor() {
72
+ super(User);
73
+ }
74
+
75
+ protected async maskPrivateFields(req: RequestContext, response: User | User[]): Promise<void> {
76
+ User.maskPrivate(response);
77
+ }
78
+
79
+ protected removeSystemFields(req: RequestContext): void {
80
+ delete req.body.id;
81
+ delete req.body.createdAt;
82
+ delete req.body.updatedAt;
83
+ }
84
+
85
+ protected async preventIDOR(req: RequestContext): Promise<void> {
86
+ // Implement your IDOR prevention logic
87
+ // For example, check if user owns the resource
88
+ }
89
+ }
90
+ ```
91
+
92
+ ### 3. Set Up Your Server
93
+
94
+ ```typescript
95
+ import { BackendServer, PGFactory } from 'unconventional';
96
+ import { UserController } from './controllers/user.controller';
97
+
98
+ const server = new BackendServer({
99
+ basePath: '/api',
100
+ getDB: PGFactory,
101
+ cors: {
102
+ origin: ['https://example.com'],
103
+ allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
104
+ },
105
+ });
106
+
107
+ const userController = new UserController();
108
+
109
+ server.app
110
+ .get('/users', userController.getAll())
111
+ .get('/users/:id', userController.get())
112
+ .post('/users', userController.create())
113
+ .put('/users/:id', userController.update())
114
+ .delete('/users/:id', userController.delete());
115
+
116
+ export default server.start();
117
+ ```
118
+
119
+ ### 4. Configure Cloudflare Workers
120
+
121
+ In your `wrangler.json`:
122
+
123
+ ```json
124
+ {
125
+ "name": "my-api",
126
+ "compatibility_date": "2024-01-01",
127
+ "kv_namespaces": [
128
+ {
129
+ "binding": "CACHE",
130
+ "id": "your-kv-namespace-id"
131
+ }
132
+ ],
133
+ "services": [
134
+ {
135
+ "binding": "DB_PROXY",
136
+ "service": "your-postgres-service"
137
+ }
138
+ ]
139
+ }
140
+ ```
141
+
142
+ ## 📚 Core Concepts
143
+
144
+ ### BaseModel
145
+
146
+ The foundation of Unconventional. `BaseModel` provides static methods for database operations and instance methods for data manipulation.
147
+
148
+ #### Static Methods
149
+
150
+ - `create(data, upsertConfig?)` - Create a new record
151
+ - `createMany(data[], upsertConfig?)` - Create multiple records
152
+ - `findById(id, config?)` - Find by ID or key
153
+ - `findOne(config)` - Find a single record matching criteria
154
+ - `findMany(config?)` - Find multiple records with pagination
155
+ - `update(id, data)` - Update a record
156
+ - `updateMany(data[])` - Update multiple records
157
+ - `delete(id)` - Delete a record
158
+ - `deleteMany(config?)` - Delete multiple records
159
+ - `count()` - Count all records
160
+ - `increment(id, data)` - Increment numeric fields
161
+ - `truncate()` - Delete all records
162
+
163
+ #### Model Configuration
164
+
165
+ ```typescript
166
+ export class Product extends BaseModel {
167
+ public static collection = 'products'; // This model's database table
168
+ public static db: DB; // The connection to the database.
169
+ // Recommend setting on BaseModel
170
+ public static idField = 'id'; // The table's id column. Default: id
171
+ public static idType = IdType.UUID; // What column type id is (number or UUID)
172
+ public static keyField = 'slug'; // Optional column used for lookups
173
+ public static ownerField = 'userId'; // For ownership checks
174
+ }
175
+ ```
176
+
177
+ ### BaseService
178
+
179
+ The service layer provides a clean interface for business logic with built-in caching support.
180
+
181
+ ```typescript
182
+ import { BaseService } from 'unconventional';
183
+
184
+ const userService = BaseService.for(User);
185
+
186
+ // Create
187
+ const user = await userService.create({ email: 'user@example.com', name: 'John' }, {
188
+ cache: ctx.env.CACHE,
189
+ cacheTTL: 3600,
190
+ });
191
+
192
+ // Get with caching
193
+ const user = await userService.get(userId, {
194
+ cache: ctx.env.CACHE,
195
+ expand: 'profile,posts',
196
+ });
197
+
198
+ // Update
199
+ const updated = await userService.update(userId, { name: 'Jane' }, {
200
+ cache: ctx.env.CACHE,
201
+ });
202
+
203
+ // Delete
204
+ await userService.delete(userId, { cache: ctx.env.CACHE });
205
+ ```
206
+
207
+ ### AbstractBaseController
208
+
209
+ Provides automatic CRUD endpoints with hooks for customization.
210
+
211
+ #### Available Methods
212
+
213
+ - `create(options?)` - POST endpoint
214
+ - `createMany(options?)` - POST endpoint for bulk creation
215
+ - `get(options?)` - GET endpoint for single resource
216
+ - `getAll(options?)` - GET endpoint for paginated list
217
+ - `update(options?)` - PUT endpoint
218
+ - `updateMany(options?)` - PUT endpoint for bulk updates
219
+ - `delete(options?)` - DELETE endpoint
220
+ - `deleteMany(options?)` - DELETE endpoint with filter
221
+
222
+ #### Controller Options
223
+
224
+ ```typescript
225
+ const controller = new MyController();
226
+
227
+ server.app.get('/users/:id', controller.get({
228
+ before: async (req) => {
229
+ // Pre-processing hook
230
+ },
231
+ after: async (req, response) => {
232
+ // Post-processing hook
233
+ },
234
+ cache: true, // Enable caching for this route
235
+ cacheTTL: 1800, // Custom TTL
236
+ download: async (req, response) => {
237
+ // Return file download instead of JSON
238
+ return { filename: 'export.json', buffer: Buffer.from(JSON.stringify(response)) };
239
+ },
240
+ }));
241
+ ```
242
+
243
+ ### BackendServer
244
+
245
+ The main server class that wraps Hono with Unconventional's defaults.
246
+
247
+ ```typescript
248
+ import { BackendServer, PGFactory } from 'unconventional';
249
+
250
+ const server = new BackendServer({
251
+ name: 'My API',
252
+ basePath: '/api/v1',
253
+ getDB: PGFactory, // Database factory function
254
+ cors: { // CORS configuration
255
+ origin: ['https://example.com'],
256
+ allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
257
+ allowHeaders: ['Content-Type', 'Authorization'],
258
+ },
259
+ middleware: [ // Middleware to run on all routes
260
+ async (ctx, next) => {
261
+ // Your middleware logic
262
+ return next();
263
+ },
264
+ ],
265
+ });
266
+
267
+ // Access the underlying Hono app
268
+ server.app.get('/health', (ctx) => ctx.json({ status: 'ok' }));
269
+
270
+ export default server.start();
271
+ ```
272
+
273
+ ## 🎨 Decorators
274
+
275
+ ### @prop
276
+
277
+ Define model properties with validation and options.
278
+
279
+ ```typescript
280
+ export class Post extends BaseModel {
281
+ @prop({ required: true })
282
+ public title!: string;
283
+
284
+ @prop({ unique: true })
285
+ public slug!: string;
286
+
287
+ @prop({ default: 'draft' })
288
+ public status!: string;
289
+
290
+ @prop({ default: () => new Date() })
291
+ public publishedAt?: Date;
292
+
293
+ @prop({ private: true })
294
+ public internalNotes?: string;
295
+
296
+ @prop({ system: true })
297
+ public systemField?: string;
298
+
299
+ @prop({
300
+ relation: {
301
+ type: RelationType.BelongsTo,
302
+ model: User,
303
+ from: 'userId',
304
+ to: 'id',
305
+ },
306
+ })
307
+ public author?: User;
308
+ }
309
+ ```
310
+
311
+ **Prop Options:**
312
+ - `required` - Field is required
313
+ - `unique` - Field has a unique index in the DB
314
+ - `default` - Default value (can be a function)
315
+ - `private` - Hide from non-privileged users (can be a function)
316
+ - `system` - Prevent editing by non-privileged users (can be a function)
317
+ - `relation` - Define relationships
318
+ - `preFormat` - Transform value before saving
319
+
320
+ ### @index
321
+
322
+ Define database indexes for performance.
323
+
324
+ ```typescript
325
+ @index({ email: 1 }, { unique: true })
326
+ @index({ createdAt: -1, status: 1 })
327
+ export class User extends BaseModel {
328
+ // ...
329
+ }
330
+ ```
331
+
332
+ ### @timestamp
333
+
334
+ Automatically manage `createdAt` and `updatedAt` fields.
335
+
336
+ ```typescript
337
+ @timestamp()
338
+ export class Post extends BaseModel {
339
+ @prop()
340
+ public createdAt?: Date;
341
+
342
+ @prop()
343
+ public updatedAt?: Date;
344
+ }
345
+
346
+ // Custom field names
347
+ @timestamp({ createdField: 'created_at', updatedField: 'updated_at' })
348
+ export class Article extends BaseModel {
349
+ // ...
350
+ }
351
+ ```
352
+
353
+ ## 🔗 Relationships
354
+
355
+ Unconventional supports four types of relationships:
356
+
357
+ ### BelongsTo
358
+
359
+ ```typescript
360
+ export class Comment extends BaseModel {
361
+ @prop({
362
+ relation: {
363
+ type: RelationType.BelongsTo,
364
+ model: Post,
365
+ from: 'postId',
366
+ to: 'id',
367
+ },
368
+ })
369
+ public post?: Post;
370
+ }
371
+ ```
372
+
373
+ ### HasOne / HasMany
374
+
375
+ ```typescript
376
+ export class User extends BaseModel {
377
+ @prop({
378
+ relation: {
379
+ type: RelationType.HasOne,
380
+ model: Profile,
381
+ from: 'id',
382
+ to: 'userId',
383
+ },
384
+ })
385
+ public profile?: Profile;
386
+
387
+ @prop({
388
+ relation: {
389
+ type: RelationType.HasMany,
390
+ model: Post,
391
+ from: 'id',
392
+ to: 'authorId',
393
+ },
394
+ })
395
+ public posts?: Post[];
396
+ }
397
+ ```
398
+
399
+ ### ManyToMany
400
+
401
+ ```typescript
402
+ export class User extends BaseModel {
403
+ @prop({
404
+ relation: {
405
+ type: RelationType.ManyToMany,
406
+ model: Role,
407
+ from: 'id',
408
+ to: 'id',
409
+ through: {
410
+ model: UserRole,
411
+ from: 'userId',
412
+ to: 'roleId',
413
+ },
414
+ },
415
+ })
416
+ public roles?: Role[];
417
+ }
418
+ ```
419
+
420
+ ### Query Expansion
421
+
422
+ Load relationships using the `expand` query parameter:
423
+
424
+ ```
425
+ GET /api/users/123?expand=profile,posts,posts.comments
426
+ ```
427
+
428
+ ## 💾 Caching
429
+
430
+ Unconventional provides intelligent caching with Cloudflare KV.
431
+
432
+ ### Automatic Caching
433
+
434
+ ```typescript
435
+ const controller = new UserController();
436
+
437
+ // Enable caching at controller level
438
+ const controller = new UserController({
439
+ cache: true,
440
+ cacheTTL: 3600, // 1 hour
441
+ });
442
+
443
+ // Or per-route
444
+ server.app.get('/users/:id', controller.get({ cache: true }));
445
+ ```
446
+
447
+ ### Manual Cache Management
448
+
449
+ ```typescript
450
+ import { BaseCache } from 'unconventional';
451
+
452
+ // Set a model in cache
453
+ await BaseCache.setModel(cache, user, undefined, 3600);
454
+
455
+ // Get a model from cache
456
+ const cached = await BaseCache.getModel(cache, User, userId);
457
+
458
+ // Clear cache
459
+ await BaseCache.clearModel(cache, user);
460
+ await BaseCache.clearPage(cache, User);
461
+ ```
462
+
463
+ ## 🔒 Security
464
+
465
+ ### IDOR Prevention
466
+
467
+ Implement ownership checks in your controller:
468
+
469
+ ```typescript
470
+ protected async preventIDOR(req: RequestContext): Promise<void> {
471
+ const ownerId = req.get(BaseEnvKey.ownerId);
472
+ const isPrivileged = req.get(BaseEnvKey.isPrivileged);
473
+
474
+ if (isPrivileged) return;
475
+
476
+ // For updates/deletes, check ownership
477
+ if (req.params.id) {
478
+ const resource = await User.findById(req.params.id);
479
+ if (resource?.userId !== ownerId) {
480
+ throw APIError.errForbidden('Access denied');
481
+ }
482
+ }
483
+
484
+ // For creates, ensure user can't set someone else as owner
485
+ if (req.body.userId && req.body.userId !== ownerId) {
486
+ throw APIError.errForbidden('Cannot create resource for another user');
487
+ }
488
+ }
489
+ ```
490
+
491
+ ### Private Fields
492
+
493
+ Fields marked with `private: true` are automatically masked:
494
+
495
+ ```typescript
496
+ @prop({ private: true })
497
+ public passwordHash?: string;
498
+
499
+ @prop({ private: (model, field) => !model.isPublic })
500
+ public internalData?: string;
501
+ ```
502
+
503
+ ### System Fields
504
+
505
+ Fields marked with `system: true` cannot be edited by non-privileged users:
506
+
507
+ ```typescript
508
+ @prop({ system: true })
509
+ public systemField?: string;
510
+
511
+ protected removeSystemFields(req: RequestContext): void {
512
+ const isPrivileged = req.get(BaseEnvKey.isPrivileged);
513
+ if (!isPrivileged) {
514
+ delete req.body.systemField;
515
+ }
516
+ }
517
+ ```
518
+
519
+ ## 📊 Querying & Pagination
520
+
521
+ ### Filtering
522
+
523
+ ```typescript
524
+ // Using query parameters
525
+ GET /api/users?filter=status eq 'active' AND age gt 18
526
+
527
+ // In code
528
+ const users = await User.findMany({
529
+ filter: 'status eq \'active\' AND age gt 18',
530
+ });
531
+ ```
532
+
533
+ ### Sorting
534
+
535
+ ```typescript
536
+ // Query parameter
537
+ GET /api/users?orderBy=createdAt desc
538
+
539
+ // In code
540
+ const users = await User.findMany({
541
+ sort: 'createdAt desc
542
+ });
543
+ ```
544
+
545
+ ### Pagination
546
+
547
+ ```typescript
548
+ // Cursor-based pagination
549
+ GET /api/users?limit=20&cursor=123
550
+
551
+ // In code
552
+ const users = await User.findMany({
553
+ limit: 20,
554
+ cursor: '123',
555
+ });
556
+
557
+ // Get all (limit < 0)
558
+ const allUsers = await User.findMany({ limit: -1 });
559
+ ```
560
+
561
+ ## 🛠️ Advanced Features
562
+
563
+ ### Custom Middleware
564
+
565
+ ```typescript
566
+ import { AbstractBaseMiddleware } from 'unconventional';
567
+
568
+ export class UserMiddleware extends AbstractBaseMiddleware<typeof User> {
569
+ constructor() {
570
+ super(User);
571
+ }
572
+
573
+ async findOwner(id: string | number): Promise<User | null> {
574
+ return this.findAncestor(id, (model) => model === User);
575
+ }
576
+ }
577
+ ```
578
+
579
+ ### Error Handling
580
+
581
+ Unconventional includes a comprehensive error handling system:
582
+
583
+ ```typescript
584
+ import { APIError } from 'unconventional';
585
+
586
+ // Predefined errors
587
+ throw APIError.errNotFound('User not found');
588
+ throw APIError.errBadRequest('Invalid input');
589
+ throw APIError.errUnauthorized('Authentication required');
590
+ throw APIError.errForbidden('Access denied');
591
+ throw APIError.errResourceCreationFailed('Failed to create user');
592
+ ```
593
+
594
+ ### Upsert Operations
595
+
596
+ ```typescript
597
+ // Create or update based on unique constraint
598
+ const user = await User.create(
599
+ { email: 'user@example.com', name: 'John' },
600
+ {
601
+ action: ConflictResolution.doUpdate,
602
+ constraint: 'email',
603
+ }
604
+ );
605
+ ```
606
+
607
+ ## 📖 Complete Example
608
+
609
+ ```typescript
610
+ // models/user.ts
611
+ import { BaseModel, prop, timestamp, RelationType } from 'unconventional';
612
+ import { Profile } from './profile';
613
+
614
+ @timestamp()
615
+ export class User extends BaseModel {
616
+ public static collection = 'users';
617
+ public static db: DB;
618
+
619
+ @prop({ required: true, unique: true })
620
+ public email!: string;
621
+
622
+ @prop({ required: true })
623
+ public name!: string;
624
+
625
+ @prop({ private: true })
626
+ public passwordHash?: string;
627
+
628
+ @prop({
629
+ relation: {
630
+ type: RelationType.HasOne,
631
+ model: Profile,
632
+ from: 'id',
633
+ to: 'userId',
634
+ },
635
+ })
636
+ public profile?: Profile;
637
+ }
638
+
639
+ // controllers/user.controller.ts
640
+ import { AbstractBaseController, APIError, BaseEnvKey } from 'unconventional';
641
+ import { User } from '../models/user';
642
+
643
+ export class UserController extends AbstractBaseController<typeof User> {
644
+ constructor() {
645
+ super(User, {
646
+ cache: true,
647
+ cacheTTL: 3600,
648
+ });
649
+ }
650
+
651
+ protected async maskPrivateFields(req: RequestContext, response: User | User[]): Promise<void> {
652
+ User.maskPrivate(response);
653
+ }
654
+
655
+ protected async preventIDOR(req: RequestContext): Promise<void> {
656
+ const ownerId = req.get(BaseEnvKey.ownerId);
657
+ const isPrivileged = req.get(BaseEnvKey.isPrivileged);
658
+
659
+ if (isPrivileged) return;
660
+
661
+ if (req.params.id) {
662
+ const user = await User.findById(req.params.id);
663
+ if (!user || user.id !== ownerId) {
664
+ throw APIError.errForbidden('Access denied');
665
+ }
666
+ }
667
+ }
668
+ }
669
+
670
+ // server.ts
671
+ import { BackendServer, PGFactory } from 'unconventional';
672
+ import { UserController } from './controllers/user.controller';
673
+
674
+ const server = new BackendServer({
675
+ basePath: '/api',
676
+ getDB: PGFactory,
677
+ cors: {
678
+ origin: ['https://example.com'],
679
+ },
680
+ });
681
+
682
+ const userController = new UserController();
683
+
684
+ server.app
685
+ .get('/users', userController.getAll())
686
+ .get('/users/:id', userController.get())
687
+ .post('/users', userController.create())
688
+ .put('/users/:id', userController.update())
689
+ .delete('/users/:id', userController.delete());
690
+
691
+ export default server.start();
692
+ ```
693
+
694
+ ## 📚 API Reference
695
+
696
+ ### BaseModel
697
+
698
+ #### Static Methods
699
+
700
+ | Method | Description | Returns |
701
+ |--------|-------------|---------|
702
+ | `create(data, upsertConfig?)` | Create a new record | `Promise<InstanceType<T> \| null>` |
703
+ | `createMany(data[], upsertConfig?)` | Create multiple records | `Promise<InstanceType<T>[] \| null>` |
704
+ | `findById(id, config?)` | Find by ID or key | `Promise<InstanceType<T> \| null>` |
705
+ | `findOne(config)` | Find a single record | `Promise<InstanceType<T> \| null>` |
706
+ | `findMany(config?)` | Find multiple records | `Promise<InstanceType<T>[]>` |
707
+ | `update(id, data)` | Update a record | `Promise<InstanceType<T> \| null>` |
708
+ | `updateMany(data[])` | Update multiple records | `Promise<InstanceType<T>[] \| null>` |
709
+ | `delete(id)` | Delete a record | `Promise<InstanceType<T> \| null>` |
710
+ | `deleteMany(config?)` | Delete multiple records | `Promise<InstanceType<T>[] \| null>` |
711
+ | `count()` | Count all records | `Promise<number \| null>` |
712
+ | `increment(id, data)` | Increment numeric fields | `Promise<InstanceType<T> \| null>` |
713
+ | `truncate()` | Delete all records | `Promise<void>` |
714
+ | `maskPrivate(data)` | Mask private fields | `void` |
715
+
716
+ #### Instance Methods
717
+
718
+ | Method | Description | Returns |
719
+ |--------|-------------|---------|
720
+ | `$id()` | Get the ID field value | `string \| number` |
721
+ | `$key()` | Get the key field value | `string \| undefined` |
722
+ | `$owner()` | Get the owner field value | `string \| number \| undefined` |
723
+
724
+ ### BaseService
725
+
726
+ | Method | Description | Returns |
727
+ |--------|-------------|---------|
728
+ | `create(data, config?)` | Create with caching | `Promise<InstanceType<M>>` |
729
+ | `createMany(data[], config?)` | Create many with caching | `Promise<InstanceType<M>[]>` |
730
+ | `get(identifier, config?)` | Get with caching | `Promise<InstanceType<M>>` |
731
+ | `getAll(config?)` | Get all with pagination | `Promise<InstanceType<M>[]>` |
732
+ | `update(identifier, data, config?)` | Update with cache invalidation | `Promise<InstanceType<M>>` |
733
+ | `updateMany(data[], config?)` | Update many | `Promise<InstanceType<M>[]>` |
734
+ | `delete(identifier, config?)` | Delete with cache invalidation | `Promise<InstanceType<M>>` |
735
+ | `deleteMany(config?)` | Delete many | `Promise<InstanceType<M>[]>` |
736
+
737
+ ### Configuration Types
738
+
739
+ #### ServerConfig
740
+
741
+ ```typescript
742
+ interface ServerConfig {
743
+ name?: string;
744
+ basePath?: string;
745
+ getDB: (ctx: Context) => DB;
746
+ cors?: CorsOptions;
747
+ middleware?: Middleware[];
748
+ }
749
+ ```
750
+
751
+ #### ControllerOptions
752
+
753
+ ```typescript
754
+ interface ControllerOptions {
755
+ cache?: boolean;
756
+ cacheTTL?: number;
757
+ }
758
+ ```
759
+
760
+ #### ServiceConfig
761
+
762
+ ```typescript
763
+ interface ServiceConfig<M> {
764
+ cache?: KVNamespace;
765
+ cacheTTL?: number;
766
+ upsertConfig?: UpsertConfig<M>;
767
+ }
768
+ ```
769
+
770
+ ## 🤝 Contributing
771
+
772
+ Contributions are welcome! Please feel free to submit a Pull Request.
773
+
774
+ 1. Fork the repository
775
+ 2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
776
+ 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
777
+ 4. Push to the branch (`git push origin feature/AmazingFeature`)
778
+ 5. Open a Pull Request
779
+
780
+ ## 📄 License
781
+
782
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
783
+
784
+ ## 👤 Author
785
+
786
+ **Dean Mauro**
787
+
788
+ - GitHub: [@cloudflare-extension](https://github.com/cloudflare-extension)
789
+ - Repository: [unconventional](https://github.com/cloudflare-extension/unconventional)
790
+
791
+ ## 🙏 Acknowledgments
792
+
793
+ - Built on [Hono](https://hono.dev/) - Ultrafast web framework
794
+ - Powered by [Cloudflare Workers](https://workers.cloudflare.com/) - Edge computing platform
795
+ - Database queries via [unconventional-pg-queries](https://github.com/cloudflare-extension/unconventional-pg-queries)
796
+
797
+ ---
798
+
799
+ Made with ❤️ for the Cloudflare ecosystem
@@ -272,7 +272,7 @@ class AbstractBaseController {
272
272
  }
273
273
  return {
274
274
  headers: ctx.req.raw.headers,
275
- waitUntil: ctx.executionCtx.waitUntil,
275
+ waitUntil: ctx.executionCtx?.waitUntil.bind(ctx.executionCtx),
276
276
  body: body,
277
277
  params: ctx.req.param(),
278
278
  queries: ctx.req.query(),
@@ -33,7 +33,7 @@ export declare class BaseModel extends Document {
33
33
  static updateMany<T extends typeof BaseModel>(this: T, data: Partial<InstanceType<T>>[]): Promise<InstanceType<T>[] | null>;
34
34
  static delete<T extends typeof BaseModel>(this: T, id: string | number): Promise<InstanceType<T> | null>;
35
35
  static deleteMany<T extends typeof BaseModel>(this: T, config?: FilterConfig): Promise<InstanceType<T>[] | null>;
36
- static count<T extends typeof BaseModel>(this: T): Promise<number | null>;
36
+ static count<T extends typeof BaseModel>(this: T, config?: FilterConfig): Promise<number | null>;
37
37
  static increment<T extends typeof BaseModel>(this: T, id: string | number, data: {
38
38
  [key in keyof InstanceType<T>]?: number;
39
39
  }): Promise<InstanceType<T> | null>;
@@ -162,11 +162,15 @@ class BaseModel extends Document {
162
162
  return null;
163
163
  return response.map(item => this.fromDatabaseJson(item));
164
164
  }
165
- static async count() {
165
+ static async count(config) {
166
+ const where = (0, db_utils_1.getWhere)(this, config?.filter);
167
+ const expand = (0, db_utils_1.expandRelationalFilters)(this, where, (0, db_utils_1.getExpand)(this, config?.expand));
166
168
  const response = await this.db.fetch({
167
169
  action: db_types_1.SqlAction.Select,
168
170
  type: unconventional_pg_queries_1.OneOrMany.Many,
169
171
  table: this.collection,
172
+ expand,
173
+ where,
170
174
  subAction: unconventional_pg_queries_1.SubAction.Count
171
175
  });
172
176
  if (!response)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unconventional",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "A complete server built on Cloudflare Workers/Pages",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "peerDependencies": {
48
48
  "hono": "^4",
49
- "unconventional-pg-queries": "^1.6.0"
49
+ "unconventional-pg-queries": "^1.7.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@cloudflare/workers-types": "^4.20231121.0",
@@ -55,6 +55,6 @@
55
55
  "hono": "^4",
56
56
  "tsx": "^4.20.6",
57
57
  "typescript": "^5.3.3",
58
- "unconventional-pg-queries": "^1.6.0"
58
+ "unconventional-pg-queries": "^1.7.0"
59
59
  }
60
60
  }