typeorm-query-scopes 0.1.0-beta.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,487 @@
1
+ # TypeORM Scopes
2
+
3
+ [![npm version](https://img.shields.io/npm/v/typeorm-query-scopes.svg)](https://www.npmjs.com/package/typeorm-query-scopes)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
6
+ [![TypeORM](https://img.shields.io/badge/TypeORM-0.3+-orange.svg)](https://typeorm.io/)
7
+
8
+ > ⚠️ **Beta Release**: This is version 0.1.0-beta.1. The API is stable but may change based on community feedback. Please report any issues on GitHub.
9
+
10
+ Sequelize-like scopes for TypeORM entities. Define reusable query filters using decorators and apply them easily to your queries.
11
+
12
+ ## Features
13
+
14
+ - 🎯 **Reusable Queries** - Define once, use everywhere
15
+ - 🔒 **Type Safe** - Full TypeScript support with autocomplete
16
+ - 🎨 **Clean Code** - Decorator-based, declarative syntax
17
+ - ⚡ **Zero Overhead** - No performance penalty
18
+ - 🔄 **Composable** - Combine multiple scopes intelligently
19
+ - 📦 **Easy Migration** - Familiar API for Sequelize users
20
+ - ✨ **Type-Safe Scope Names** - IDE autocomplete and compile-time validation
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install typeorm-query-scopes@beta
26
+ # or
27
+ yarn add typeorm-query-scopes@beta
28
+ # or
29
+ pnpm add typeorm-query-scopes@beta
30
+ ```
31
+
32
+ Make sure you have TypeORM installed as a peer dependency:
33
+
34
+ ```bash
35
+ npm install typeorm reflect-metadata
36
+ ```
37
+
38
+ Enable decorators in your `tsconfig.json`:
39
+
40
+ ```json
41
+ {
42
+ "compilerOptions": {
43
+ "experimentalDecorators": true,
44
+ "emitDecoratorMetadata": true
45
+ }
46
+ }
47
+ ```
48
+
49
+ ## Features
50
+
51
+ - 🎯 **Decorator-based scope definitions** - Define scopes directly on your entities
52
+ - 🔄 **Default scopes** - Automatically applied to all queries
53
+ - 🔗 **Scope merging** - Combine multiple scopes intelligently
54
+ - 📦 **Function scopes** - Dynamic scopes with parameters
55
+ - 🎨 **TypeScript support** - Full type safety
56
+ - ⚡ **Easy to use** - Similar API to Sequelize scopes
57
+
58
+ ## Quick Start
59
+
60
+ 👉 **[Quick Start Guide](./QUICK_START.md)** - Get started in 5 minutes!
61
+
62
+ ### 1. Define Scopes on Your Entity
63
+
64
+ ```typescript
65
+ import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
66
+ import { Scopes, DefaultScope } from 'typeorm-query-scopes';
67
+
68
+ @DefaultScope<User>({
69
+ where: { isActive: true }
70
+ })
71
+ @Scopes<User>({
72
+ // Simple scope with where condition
73
+ verified: {
74
+ where: { isVerified: true }
75
+ },
76
+
77
+ // Scope with relations
78
+ withPosts: {
79
+ relations: { posts: true }
80
+ },
81
+
82
+ // Scope with ordering
83
+ newest: {
84
+ order: { createdAt: 'DESC' }
85
+ },
86
+
87
+ // Function scope with parameters
88
+ byRole: (role: string) => ({
89
+ where: { role }
90
+ }),
91
+
92
+ // Complex scope
93
+ premium: {
94
+ where: {
95
+ subscriptionType: 'premium',
96
+ subscriptionExpiry: MoreThan(new Date())
97
+ },
98
+ relations: { subscription: true }
99
+ }
100
+ })
101
+ @Entity()
102
+ export class User {
103
+ @PrimaryGeneratedColumn()
104
+ id: number;
105
+
106
+ @Column()
107
+ email: string;
108
+
109
+ @Column()
110
+ role: string;
111
+
112
+ @Column({ default: true })
113
+ isActive: boolean;
114
+
115
+ @Column({ default: false })
116
+ isVerified: boolean;
117
+
118
+ @Column()
119
+ createdAt: Date;
120
+ }
121
+ ```
122
+
123
+ > 💡 **Type-Safe Scopes**: By adding the second generic parameter with scope names, you get IDE autocomplete and compile-time validation when calling `.scope("scopeName")`!
124
+
125
+ ### 2. Use Scopes in Your Queries
126
+
127
+ ```typescript
128
+ import { getScopedRepository } from 'typeorm-query-scopes';
129
+ import { dataSource } from './data-source';
130
+ import { User } from './entities/User';
131
+
132
+ // Create a scoped repository
133
+ const userRepo = getScopedRepository(User, dataSource);
134
+
135
+ // Apply single scope
136
+ const verifiedUsers = await userRepo.scope('verified').find();
137
+
138
+ // Apply multiple scopes
139
+ const verifiedWithPosts = await userRepo
140
+ .scope('verified', 'withPosts')
141
+ .find();
142
+
143
+ // Apply function scope with parameters
144
+ const admins = await userRepo
145
+ .scope({ method: ['byRole', 'admin'] })
146
+ .find();
147
+
148
+ // Combine scopes and additional options
149
+ const recentAdmins = await userRepo
150
+ .scope('newest', { method: ['byRole', 'admin'] })
151
+ .find({ take: 10 });
152
+
153
+ // Remove default scope
154
+ const allUsers = await userRepo.unscoped().find();
155
+
156
+ // Apply default scope explicitly with other scopes
157
+ const activeVerified = await userRepo
158
+ .scope('defaultScope', 'verified')
159
+ .find();
160
+ ```
161
+
162
+ ## API Reference
163
+
164
+ ### Type-Safe Scope Names
165
+
166
+ For better IDE support and compile-time safety, you can define scope names as types:
167
+
168
+ ```typescript
169
+ @Scopes<User, {
170
+ active: any;
171
+ verified: any;
172
+ byRole: any;
173
+ }>({
174
+ active: { where: { isActive: true } },
175
+ verified: { where: { isVerified: true } },
176
+ byRole: (role: string) => ({ where: { role } })
177
+ })
178
+ @Entity()
179
+ class User { ... }
180
+
181
+ // Now TypeScript knows the available scope names!
182
+ const repo = getScopedRepository(User, dataSource);
183
+
184
+ // ✅ TypeScript autocompletes: 'active' | 'verified' | 'byRole'
185
+ repo.scope('active') // IDE shows autocomplete!
186
+
187
+ // ❌ TypeScript error: 'invalid' is not a valid scope name
188
+ repo.scope('invalid') // Compile error!
189
+ ```
190
+
191
+ **Benefits:**
192
+ - IDE autocomplete for scope names
193
+ - Compile-time error checking
194
+ - Better refactoring support
195
+ - Self-documenting code
196
+
197
+ ### Decorators
198
+
199
+ #### `@Scopes<Entity>(scopes)`
200
+
201
+ Define named scopes on an entity.
202
+
203
+ ```typescript
204
+ @Scopes<User>({
205
+ scopeName: { where: { field: value } },
206
+ dynamicScope: (param) => ({ where: { field: param } })
207
+ })
208
+ ```
209
+
210
+ #### `@DefaultScope<Entity>(scope)`
211
+
212
+ Define a default scope that's automatically applied to all queries.
213
+
214
+ ```typescript
215
+ @DefaultScope<User>({
216
+ where: { isActive: true }
217
+ })
218
+ ```
219
+
220
+ ### ScopedRepository Methods
221
+
222
+ #### `scope(...scopeNames)`
223
+
224
+ Apply one or more scopes to the repository.
225
+
226
+ ```typescript
227
+ // Single scope
228
+ repo.scope('active')
229
+
230
+ // Multiple scopes
231
+ repo.scope('active', 'verified')
232
+
233
+ // Function scope with parameters
234
+ repo.scope({ method: ['byRole', 'admin'] })
235
+
236
+ // Include default scope explicitly
237
+ repo.scope('defaultScope', 'verified')
238
+ ```
239
+
240
+ #### `unscoped()`
241
+
242
+ Remove all scopes including the default scope.
243
+
244
+ ```typescript
245
+ repo.unscoped().find()
246
+ ```
247
+
248
+ #### `find(options?)`
249
+
250
+ Find entities with applied scopes.
251
+
252
+ ```typescript
253
+ await repo.scope('active').find({ take: 10 })
254
+ ```
255
+
256
+ #### `findOne(options)`
257
+
258
+ Find one entity with applied scopes.
259
+
260
+ ```typescript
261
+ await repo.scope('active').findOne({ where: { id: 1 } })
262
+ ```
263
+
264
+ #### `findOneBy(where)`
265
+
266
+ Find one entity by conditions with applied scopes.
267
+
268
+ ```typescript
269
+ await repo.scope('active').findOneBy({ email: 'user@example.com' })
270
+ ```
271
+
272
+ #### `count(options?)`
273
+
274
+ Count entities with applied scopes.
275
+
276
+ ```typescript
277
+ await repo.scope('active').count()
278
+ ```
279
+
280
+ #### `findAndCount(options?)`
281
+
282
+ Find and count entities with applied scopes.
283
+
284
+ ```typescript
285
+ const [users, total] = await repo.scope('active').findAndCount({ take: 10 })
286
+ ```
287
+
288
+ ## Scope Options
289
+
290
+ Scopes support the following TypeORM find options:
291
+
292
+ - `where` - Filter conditions (merged with AND logic)
293
+ - `relations` - Relations to load
294
+ - `order` - Sorting options
295
+ - `select` - Fields to select
296
+ - `skip` - Number of records to skip
297
+ - `take` - Number of records to take
298
+ - `cache` - Query caching options
299
+
300
+ ## Scope Merging
301
+
302
+ When multiple scopes are applied, they are merged intelligently:
303
+
304
+ - **where**: Merged using AND logic
305
+ - **relations**: Combined (all relations loaded)
306
+ - **order**: Later scopes override earlier ones
307
+ - **select**: Combined (union of all fields)
308
+ - **skip/take/cache**: Last scope wins
309
+
310
+ ```typescript
311
+ @Scopes<User>({
312
+ scope1: {
313
+ where: { isActive: true },
314
+ order: { createdAt: 'DESC' },
315
+ take: 10
316
+ },
317
+ scope2: {
318
+ where: { isVerified: true },
319
+ take: 20
320
+ }
321
+ })
322
+
323
+ // Applying both scopes results in:
324
+ // where: { isActive: true, isVerified: true }
325
+ // order: { createdAt: 'DESC' }
326
+ // take: 20 (scope2 overrides scope1)
327
+ ```
328
+
329
+ ## Advanced Examples
330
+
331
+ ### Scope with Complex Conditions
332
+
333
+ ```typescript
334
+ import { MoreThan, LessThan, In } from 'typeorm';
335
+
336
+ @Scopes<Product>({
337
+ inStock: {
338
+ where: { quantity: MoreThan(0) }
339
+ },
340
+
341
+ inPriceRange: (min: number, max: number) => ({
342
+ where: {
343
+ price: MoreThan(min),
344
+ price: LessThan(max)
345
+ }
346
+ }),
347
+
348
+ byCategories: (categories: string[]) => ({
349
+ where: { category: In(categories) }
350
+ })
351
+ })
352
+ ```
353
+
354
+ ### Scope with Nested Relations
355
+
356
+ ```typescript
357
+ @Scopes<Post>({
358
+ withAuthorAndComments: {
359
+ relations: {
360
+ author: true,
361
+ comments: {
362
+ user: true
363
+ }
364
+ }
365
+ },
366
+
367
+ published: {
368
+ where: {
369
+ status: 'published',
370
+ publishedAt: LessThan(new Date())
371
+ }
372
+ }
373
+ })
374
+ ```
375
+
376
+ ### Reusable Scope Repository
377
+
378
+ ```typescript
379
+ // Create a service with scoped repository
380
+ export class UserService {
381
+ private userRepo: ScopedRepository<User>;
382
+
383
+ constructor(private dataSource: DataSource) {
384
+ this.userRepo = getScopedRepository(User, dataSource);
385
+ }
386
+
387
+ async getActiveUsers() {
388
+ return this.userRepo.scope('active').find();
389
+ }
390
+
391
+ async getAdmins() {
392
+ return this.userRepo
393
+ .scope({ method: ['byRole', 'admin'] })
394
+ .find();
395
+ }
396
+
397
+ async getAllUsersIncludingInactive() {
398
+ return this.userRepo.unscoped().find();
399
+ }
400
+ }
401
+ ```
402
+
403
+ ## Comparison with Sequelize
404
+
405
+ This package brings Sequelize-style scopes to TypeORM:
406
+
407
+ | Feature | Sequelize | TypeORM Scopes |
408
+ |---------|-----------|----------------|
409
+ | Default scope | ✅ | ✅ |
410
+ | Named scopes | ✅ | ✅ |
411
+ | Function scopes | ✅ | ✅ |
412
+ | Scope merging | ✅ | ✅ |
413
+ | Unscoped queries | ✅ | ✅ |
414
+ | Decorator-based | ❌ | ✅ |
415
+
416
+ ## Documentation
417
+
418
+ - [Type-Safe Scopes Guide](./TYPE_SAFE_SCOPES.md) - Complete guide to type-safe scope names
419
+ - [Migration from Sequelize](./MIGRATION_FROM_SEQUELIZE.md) - Guide for migrating from Sequelize scopes
420
+ - [Basic Usage Example](./examples/basic-usage.ts) - Simple examples to get started
421
+ - [Advanced Usage Example](./examples/advanced-usage.ts) - Complex scope patterns
422
+ - [Type-Safe Scopes Example](./examples/type-safe-scopes.ts) - Type-safe scope demonstration
423
+ - [Real-world Example](./examples/real-world-example.ts) - Complete blog application example
424
+ - [Contributing Guide](./CONTRIBUTING.md) - How to contribute to the project
425
+ - [Changelog](./CHANGELOG.md) - Version history and changes
426
+
427
+ ## Roadmap
428
+
429
+ Future enhancements being considered:
430
+
431
+ - [ ] Support for `update()` and `delete()` operations with scopes
432
+ - [ ] Scope inheritance for entity inheritance
433
+ - [ ] Query builder integration
434
+ - [ ] Performance optimizations
435
+ - [ ] Additional scope merging strategies
436
+ - [ ] Scope validation and debugging tools
437
+
438
+ ## FAQ
439
+
440
+ ### Q: Can I use this with NestJS?
441
+
442
+ Yes! TypeORM Scopes works perfectly with NestJS. Just inject the DataSource and create scoped repositories:
443
+
444
+ ```typescript
445
+ @Injectable()
446
+ export class UserService {
447
+ private userRepo: ScopedRepository<User>;
448
+
449
+ constructor(private dataSource: DataSource) {
450
+ this.userRepo = getScopedRepository(User, dataSource);
451
+ }
452
+
453
+ async getActiveUsers() {
454
+ return this.userRepo.scope('active').find();
455
+ }
456
+ }
457
+ ```
458
+
459
+ ### Q: Does this work with MongoDB?
460
+
461
+ Yes, TypeORM Scopes works with any database supported by TypeORM, including MongoDB.
462
+
463
+ ### Q: Can I combine scopes with QueryBuilder?
464
+
465
+ Currently, scopes work with the repository pattern. QueryBuilder integration is planned for a future release.
466
+
467
+ ### Q: How does performance compare to regular TypeORM queries?
468
+
469
+ Scopes add minimal overhead - they simply merge options before executing the query. The actual database query performance is identical to regular TypeORM queries.
470
+
471
+ ## License
472
+
473
+ MIT - see [LICENSE](./LICENSE) file for details
474
+
475
+ ## Contributing
476
+
477
+ Contributions are welcome! Please read our [Contributing Guide](./CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
478
+
479
+ ## Support
480
+
481
+ - 📖 [Documentation](./README.md)
482
+ - 🐛 [Issue Tracker](https://github.com/yourusername/typeorm-query-scopes/issues)
483
+ - 💬 [Discussions](https://github.com/yourusername/typeorm-query-scopes/discussions)
484
+
485
+ ## Acknowledgments
486
+
487
+ Inspired by [Sequelize scopes](https://sequelize.org/docs/v7/other-topics/scopes/) - bringing the same powerful pattern to TypeORM.
@@ -0,0 +1,28 @@
1
+ import { ScopeDefinition, ScopeOptions } from './types';
2
+ /**
3
+ * Decorator to define scopes on an entity with type-safe scope names
4
+ * @example
5
+ * // Without type-safe names:
6
+ * @Scopes<User>({
7
+ * active: { where: { isActive: true } }
8
+ * })
9
+ *
10
+ * // With type-safe names:
11
+ * @Scopes<User, { active: any; verified: any }>({
12
+ * active: { where: { isActive: true } },
13
+ * verified: { where: { isVerified: true } }
14
+ * })
15
+ * @Entity()
16
+ * class User { ... }
17
+ */
18
+ export declare function Scopes<T, S extends Record<string, ScopeDefinition<T>> = Record<string, ScopeDefinition<T>>>(scopes: S): <C extends new (...args: any[]) => T>(target: C) => C & {
19
+ __scopeNames?: keyof S;
20
+ };
21
+ /**
22
+ * Decorator to define a default scope on an entity
23
+ * @example
24
+ * @DefaultScope<User>({ where: { isActive: true } })
25
+ * @Entity()
26
+ * class User { ... }
27
+ */
28
+ export declare function DefaultScope<T>(scope: ScopeOptions<T>): (target: Function) => void;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Scopes = Scopes;
4
+ exports.DefaultScope = DefaultScope;
5
+ const metadata_1 = require("./metadata");
6
+ /**
7
+ * Decorator to define scopes on an entity with type-safe scope names
8
+ * @example
9
+ * // Without type-safe names:
10
+ * @Scopes<User>({
11
+ * active: { where: { isActive: true } }
12
+ * })
13
+ *
14
+ * // With type-safe names:
15
+ * @Scopes<User, { active: any; verified: any }>({
16
+ * active: { where: { isActive: true } },
17
+ * verified: { where: { isVerified: true } }
18
+ * })
19
+ * @Entity()
20
+ * class User { ... }
21
+ */
22
+ function Scopes(scopes) {
23
+ return function (target) {
24
+ Object.entries(scopes).forEach(([name, scope]) => {
25
+ metadata_1.ScopeMetadataStorage.addScope(target, name, scope);
26
+ });
27
+ return target;
28
+ };
29
+ }
30
+ /**
31
+ * Decorator to define a default scope on an entity
32
+ * @example
33
+ * @DefaultScope<User>({ where: { isActive: true } })
34
+ * @Entity()
35
+ * class User { ... }
36
+ */
37
+ function DefaultScope(scope) {
38
+ return function (target) {
39
+ metadata_1.ScopeMetadataStorage.setDefaultScope(target, scope);
40
+ };
41
+ }
@@ -0,0 +1,14 @@
1
+ export { Scopes, DefaultScope } from './decorators';
2
+ export { ScopedRepository } from './scoped-repository';
3
+ export { ScopeMetadataStorage } from './metadata';
4
+ export { ScopeMerger } from './scope-merger';
5
+ export type { ScopeOptions, ScopeFunction, ScopeDefinition, ScopeMetadata, ScopeName, ExtractScopeNames } from './types';
6
+ import { DataSource, ObjectLiteral } from 'typeorm';
7
+ import { ScopedRepository } from './scoped-repository';
8
+ /**
9
+ * Create a scoped repository for an entity
10
+ * @example
11
+ * const userRepo = getScopedRepository(User, dataSource);
12
+ * const activeUsers = await userRepo.scope('active').find();
13
+ */
14
+ export declare function getScopedRepository<Entity extends ObjectLiteral, EntityClass extends new (...args: any[]) => Entity = any>(entity: EntityClass, dataSource: DataSource): ScopedRepository<Entity, EntityClass>;
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ScopeMerger = exports.ScopeMetadataStorage = exports.ScopedRepository = exports.DefaultScope = exports.Scopes = void 0;
4
+ exports.getScopedRepository = getScopedRepository;
5
+ var decorators_1 = require("./decorators");
6
+ Object.defineProperty(exports, "Scopes", { enumerable: true, get: function () { return decorators_1.Scopes; } });
7
+ Object.defineProperty(exports, "DefaultScope", { enumerable: true, get: function () { return decorators_1.DefaultScope; } });
8
+ var scoped_repository_1 = require("./scoped-repository");
9
+ Object.defineProperty(exports, "ScopedRepository", { enumerable: true, get: function () { return scoped_repository_1.ScopedRepository; } });
10
+ var metadata_1 = require("./metadata");
11
+ Object.defineProperty(exports, "ScopeMetadataStorage", { enumerable: true, get: function () { return metadata_1.ScopeMetadataStorage; } });
12
+ var scope_merger_1 = require("./scope-merger");
13
+ Object.defineProperty(exports, "ScopeMerger", { enumerable: true, get: function () { return scope_merger_1.ScopeMerger; } });
14
+ const scoped_repository_2 = require("./scoped-repository");
15
+ /**
16
+ * Create a scoped repository for an entity
17
+ * @example
18
+ * const userRepo = getScopedRepository(User, dataSource);
19
+ * const activeUsers = await userRepo.scope('active').find();
20
+ */
21
+ function getScopedRepository(entity, dataSource) {
22
+ return new scoped_repository_2.ScopedRepository(entity, dataSource);
23
+ }
@@ -0,0 +1,7 @@
1
+ import { ScopeMetadata } from './types';
2
+ export declare class ScopeMetadataStorage {
3
+ private static storage;
4
+ static getMetadata<T>(target: Function): ScopeMetadata<T>;
5
+ static setDefaultScope<T>(target: Function, scope: any): void;
6
+ static addScope<T>(target: Function, name: string, scope: any): void;
7
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ScopeMetadataStorage = void 0;
4
+ const SCOPE_METADATA_KEY = Symbol('typeorm:scopes');
5
+ class ScopeMetadataStorage {
6
+ static getMetadata(target) {
7
+ if (!this.storage.has(target)) {
8
+ this.storage.set(target, {
9
+ scopes: new Map(),
10
+ });
11
+ }
12
+ return this.storage.get(target);
13
+ }
14
+ static setDefaultScope(target, scope) {
15
+ const metadata = this.getMetadata(target);
16
+ metadata.defaultScope = scope;
17
+ }
18
+ static addScope(target, name, scope) {
19
+ const metadata = this.getMetadata(target);
20
+ metadata.scopes.set(name, scope);
21
+ }
22
+ }
23
+ exports.ScopeMetadataStorage = ScopeMetadataStorage;
24
+ ScopeMetadataStorage.storage = new Map();
@@ -0,0 +1,8 @@
1
+ import { ScopeOptions } from './types';
2
+ export declare class ScopeMerger {
3
+ /**
4
+ * Merges multiple scope options into a single options object
5
+ * Similar to Sequelize's scope merging behavior
6
+ */
7
+ static merge<T>(...scopes: ScopeOptions<T>[]): ScopeOptions<T>;
8
+ }
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ScopeMerger = void 0;
4
+ class ScopeMerger {
5
+ /**
6
+ * Merges multiple scope options into a single options object
7
+ * Similar to Sequelize's scope merging behavior
8
+ */
9
+ static merge(...scopes) {
10
+ const result = {};
11
+ for (const scope of scopes) {
12
+ // Merge where conditions using AND logic
13
+ if (scope.where) {
14
+ if (!result.where) {
15
+ result.where = Array.isArray(scope.where) ? [...scope.where] : { ...scope.where };
16
+ }
17
+ else {
18
+ // Merge where conditions
19
+ if (Array.isArray(result.where) && Array.isArray(scope.where)) {
20
+ result.where = [...result.where, ...scope.where];
21
+ }
22
+ else if (Array.isArray(result.where)) {
23
+ result.where = [...result.where, scope.where];
24
+ }
25
+ else if (Array.isArray(scope.where)) {
26
+ result.where = [result.where, ...scope.where];
27
+ }
28
+ else {
29
+ result.where = { ...result.where, ...scope.where };
30
+ }
31
+ }
32
+ }
33
+ // Merge relations
34
+ if (scope.relations) {
35
+ result.relations = {
36
+ ...(result.relations || {}),
37
+ ...scope.relations,
38
+ };
39
+ }
40
+ // Merge order (later scopes override)
41
+ if (scope.order) {
42
+ result.order = {
43
+ ...(result.order || {}),
44
+ ...scope.order,
45
+ };
46
+ }
47
+ // Merge select (combine unique fields)
48
+ if (scope.select) {
49
+ if (!result.select) {
50
+ result.select = [...scope.select];
51
+ }
52
+ else {
53
+ const combined = new Set([...result.select, ...scope.select]);
54
+ result.select = Array.from(combined);
55
+ }
56
+ }
57
+ // Override scalar values (last scope wins)
58
+ if (scope.skip !== undefined)
59
+ result.skip = scope.skip;
60
+ if (scope.take !== undefined)
61
+ result.take = scope.take;
62
+ if (scope.cache !== undefined)
63
+ result.cache = scope.cache;
64
+ }
65
+ return result;
66
+ }
67
+ }
68
+ exports.ScopeMerger = ScopeMerger;
@@ -0,0 +1,54 @@
1
+ import { FindManyOptions, FindOneOptions, ObjectLiteral, EntityTarget, DataSource } from 'typeorm';
2
+ type ExtractScopeNames<T> = T extends {
3
+ __scopeNames?: infer S;
4
+ } ? S : string;
5
+ export declare class ScopedRepository<Entity extends ObjectLiteral, EntityClass = any> {
6
+ private target;
7
+ private dataSource;
8
+ private appliedScopes;
9
+ private includeDefaultScope;
10
+ constructor(target: EntityTarget<Entity>, dataSource: DataSource);
11
+ private get repository();
12
+ /**
13
+ * Apply one or more scopes to the repository
14
+ * @example
15
+ * userRepo.scope('active', 'withPosts').find()
16
+ * userRepo.scope({ method: ['byRole', 'admin'] }).find()
17
+ */
18
+ scope(...scopeNames: (ExtractScopeNames<EntityClass> | {
19
+ method: [ExtractScopeNames<EntityClass>, ...any[]];
20
+ } | null)[]): ScopedRepository<Entity, EntityClass>;
21
+ /**
22
+ * Remove the default scope
23
+ * @example
24
+ * userRepo.unscoped().find()
25
+ */
26
+ unscoped(): ScopedRepository<Entity, EntityClass>;
27
+ /**
28
+ * Get merged find options with all applied scopes
29
+ */
30
+ private getMergedOptions;
31
+ /**
32
+ * Find entities with applied scopes
33
+ */
34
+ find(options?: FindManyOptions<Entity>): Promise<Entity[]>;
35
+ /**
36
+ * Find one entity with applied scopes
37
+ */
38
+ findOne(options: FindOneOptions<Entity>): Promise<Entity | null>;
39
+ /**
40
+ * Find one entity by ID with applied scopes
41
+ */
42
+ findOneBy(where: any): Promise<Entity | null>;
43
+ /**
44
+ * Count entities with applied scopes
45
+ */
46
+ count(options?: FindManyOptions<Entity>): Promise<number>;
47
+ /**
48
+ * Find and count entities with applied scopes
49
+ */
50
+ findAndCount(options?: FindManyOptions<Entity>): Promise<[Entity[], number]>;
51
+ private clone;
52
+ private getEntityConstructor;
53
+ }
54
+ export {};
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ScopedRepository = void 0;
4
+ const metadata_1 = require("./metadata");
5
+ const scope_merger_1 = require("./scope-merger");
6
+ class ScopedRepository {
7
+ constructor(target, dataSource) {
8
+ this.target = target;
9
+ this.dataSource = dataSource;
10
+ this.appliedScopes = [];
11
+ this.includeDefaultScope = true;
12
+ }
13
+ get repository() {
14
+ return this.dataSource.getRepository(this.target);
15
+ }
16
+ /**
17
+ * Apply one or more scopes to the repository
18
+ * @example
19
+ * userRepo.scope('active', 'withPosts').find()
20
+ * userRepo.scope({ method: ['byRole', 'admin'] }).find()
21
+ */
22
+ scope(...scopeNames) {
23
+ const cloned = this.clone();
24
+ const metadata = metadata_1.ScopeMetadataStorage.getMetadata(this.getEntityConstructor());
25
+ for (const scopeName of scopeNames) {
26
+ if (scopeName === null || scopeName === 'defaultScope') {
27
+ cloned.includeDefaultScope = true;
28
+ continue;
29
+ }
30
+ if (typeof scopeName === 'string') {
31
+ const scopeDef = metadata.scopes.get(scopeName);
32
+ if (!scopeDef) {
33
+ throw new Error(`Scope "${scopeName}" not found on entity`);
34
+ }
35
+ const scopeOptions = typeof scopeDef === 'function' ? scopeDef() : scopeDef;
36
+ cloned.appliedScopes.push(scopeOptions);
37
+ }
38
+ else if (typeof scopeName === 'object' && 'method' in scopeName && scopeName.method) {
39
+ const [name, ...args] = scopeName.method;
40
+ const scopeDef = metadata.scopes.get(name);
41
+ if (!scopeDef) {
42
+ throw new Error(`Scope "${String(name)}" not found on entity`);
43
+ }
44
+ if (typeof scopeDef !== 'function') {
45
+ throw new Error(`Scope "${String(name)}" is not a function`);
46
+ }
47
+ const scopeOptions = scopeDef(...args);
48
+ cloned.appliedScopes.push(scopeOptions);
49
+ }
50
+ }
51
+ return cloned;
52
+ }
53
+ /**
54
+ * Remove the default scope
55
+ * @example
56
+ * userRepo.unscoped().find()
57
+ */
58
+ unscoped() {
59
+ const cloned = this.clone();
60
+ cloned.includeDefaultScope = false;
61
+ cloned.appliedScopes = [];
62
+ return cloned;
63
+ }
64
+ /**
65
+ * Get merged find options with all applied scopes
66
+ */
67
+ getMergedOptions(options = {}) {
68
+ const scopesToApply = [];
69
+ // Add default scope if enabled
70
+ if (this.includeDefaultScope) {
71
+ const metadata = metadata_1.ScopeMetadataStorage.getMetadata(this.getEntityConstructor());
72
+ if (metadata.defaultScope) {
73
+ scopesToApply.push(metadata.defaultScope);
74
+ }
75
+ }
76
+ // Add applied scopes
77
+ scopesToApply.push(...this.appliedScopes);
78
+ // Add user-provided options
79
+ scopesToApply.push(options);
80
+ return scope_merger_1.ScopeMerger.merge(...scopesToApply);
81
+ }
82
+ /**
83
+ * Find entities with applied scopes
84
+ */
85
+ async find(options) {
86
+ const mergedOptions = this.getMergedOptions(options);
87
+ return this.repository.find(mergedOptions);
88
+ }
89
+ /**
90
+ * Find one entity with applied scopes
91
+ */
92
+ async findOne(options) {
93
+ const mergedOptions = this.getMergedOptions(options);
94
+ return this.repository.findOne(mergedOptions);
95
+ }
96
+ /**
97
+ * Find one entity by ID with applied scopes
98
+ */
99
+ async findOneBy(where) {
100
+ return this.findOne({ where });
101
+ }
102
+ /**
103
+ * Count entities with applied scopes
104
+ */
105
+ async count(options) {
106
+ const mergedOptions = this.getMergedOptions(options);
107
+ return this.repository.count(mergedOptions);
108
+ }
109
+ /**
110
+ * Find and count entities with applied scopes
111
+ */
112
+ async findAndCount(options) {
113
+ const mergedOptions = this.getMergedOptions(options);
114
+ return this.repository.findAndCount(mergedOptions);
115
+ }
116
+ clone() {
117
+ const cloned = new ScopedRepository(this.target, this.dataSource);
118
+ cloned.appliedScopes = [...this.appliedScopes];
119
+ cloned.includeDefaultScope = this.includeDefaultScope;
120
+ return cloned;
121
+ }
122
+ getEntityConstructor() {
123
+ if (typeof this.target === 'function') {
124
+ return this.target;
125
+ }
126
+ throw new Error('Entity target must be a constructor function');
127
+ }
128
+ }
129
+ exports.ScopedRepository = ScopedRepository;
@@ -0,0 +1,20 @@
1
+ import { FindOptionsWhere, FindOptionsOrder, FindOptionsRelations } from 'typeorm';
2
+ export type ScopeOptions<T> = {
3
+ where?: FindOptionsWhere<T> | FindOptionsWhere<T>[];
4
+ relations?: FindOptionsRelations<T>;
5
+ order?: FindOptionsOrder<T>;
6
+ skip?: number;
7
+ take?: number;
8
+ select?: (keyof T)[];
9
+ cache?: boolean | number;
10
+ };
11
+ export type ScopeFunction<T> = (...args: any[]) => ScopeOptions<T>;
12
+ export type ScopeDefinition<T> = ScopeOptions<T> | ScopeFunction<T>;
13
+ export interface ScopeMetadata<T = any> {
14
+ defaultScope?: ScopeOptions<T>;
15
+ scopes: Map<string, ScopeDefinition<T>>;
16
+ }
17
+ export type ScopeName<T> = T extends {
18
+ __scopeNames?: infer S;
19
+ } ? S : string;
20
+ export type ExtractScopeNames<T> = T extends Record<infer K, any> ? K : never;
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "typeorm-query-scopes",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "Sequelize-like scopes for TypeORM entities - Define reusable query filters with decorators",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "npx ts-node test/integration.test.ts",
10
+ "demo": "npx ts-node examples/demo.ts",
11
+ "docs:dev": "vitepress dev docs",
12
+ "docs:build": "vitepress build docs",
13
+ "docs:preview": "vitepress preview docs",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "keywords": [
17
+ "typeorm",
18
+ "scopes",
19
+ "orm",
20
+ "database",
21
+ "query",
22
+ "decorator",
23
+ "typescript",
24
+ "sequelize",
25
+ "repository",
26
+ "filter",
27
+ "reusable-queries",
28
+ "type-safe"
29
+ ],
30
+ "author": "aramyounis",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/aramyounis/typeorm-query-scopes.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/aramyounis/typeorm-query-scopes/issues"
38
+ },
39
+ "homepage": "https://github.com/aramyounis/typeorm-query-scopes#readme",
40
+ "peerDependencies": {
41
+ "typeorm": "^0.3.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^20.0.0",
45
+ "better-sqlite3": "^12.6.2",
46
+ "sqlite3": "^5.1.7",
47
+ "typeorm": "^0.3.0",
48
+ "typescript": "^5.0.0",
49
+ "vitepress": "^1.6.4",
50
+ "vue": "^3.5.29"
51
+ },
52
+ "files": [
53
+ "dist",
54
+ "README.md",
55
+ "LICENSE",
56
+ "CHANGELOG.md",
57
+ "TYPE_SAFE_SCOPES.md",
58
+ "MIGRATION_FROM_SEQUELIZE.md"
59
+ ],
60
+ "engines": {
61
+ "node": ">=16.0.0"
62
+ }
63
+ }