express-model-binding 1.0.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/LICENSE +21 -0
- package/README.md +208 -0
- package/dist/BaseAdapter-BjvLQijd.d.mts +214 -0
- package/dist/BaseAdapter-BjvLQijd.d.ts +214 -0
- package/dist/adapters/KnexAdapter.d.mts +44 -0
- package/dist/adapters/KnexAdapter.d.ts +44 -0
- package/dist/adapters/KnexAdapter.js +257 -0
- package/dist/adapters/KnexAdapter.js.map +1 -0
- package/dist/adapters/KnexAdapter.mjs +229 -0
- package/dist/adapters/KnexAdapter.mjs.map +1 -0
- package/dist/adapters/MongooseAdapter.d.mts +30 -0
- package/dist/adapters/MongooseAdapter.d.ts +30 -0
- package/dist/adapters/MongooseAdapter.js +245 -0
- package/dist/adapters/MongooseAdapter.js.map +1 -0
- package/dist/adapters/MongooseAdapter.mjs +225 -0
- package/dist/adapters/MongooseAdapter.mjs.map +1 -0
- package/dist/adapters/PrismaAdapter.d.mts +42 -0
- package/dist/adapters/PrismaAdapter.d.ts +42 -0
- package/dist/adapters/PrismaAdapter.js +247 -0
- package/dist/adapters/PrismaAdapter.js.map +1 -0
- package/dist/adapters/PrismaAdapter.mjs +220 -0
- package/dist/adapters/PrismaAdapter.mjs.map +1 -0
- package/dist/adapters/SequelizeAdapter.d.mts +27 -0
- package/dist/adapters/SequelizeAdapter.d.ts +27 -0
- package/dist/adapters/SequelizeAdapter.js +280 -0
- package/dist/adapters/SequelizeAdapter.js.map +1 -0
- package/dist/adapters/SequelizeAdapter.mjs +260 -0
- package/dist/adapters/SequelizeAdapter.mjs.map +1 -0
- package/dist/adapters/TypeORMAdapter.d.mts +26 -0
- package/dist/adapters/TypeORMAdapter.d.ts +26 -0
- package/dist/adapters/TypeORMAdapter.js +294 -0
- package/dist/adapters/TypeORMAdapter.js.map +1 -0
- package/dist/adapters/TypeORMAdapter.mjs +267 -0
- package/dist/adapters/TypeORMAdapter.mjs.map +1 -0
- package/dist/index.d.mts +411 -0
- package/dist/index.d.ts +411 -0
- package/dist/index.js +1514 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1450 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +148 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Sepehr Mohseni
|
|
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,208 @@
|
|
|
1
|
+
# express-model-binding
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/express-model-binding)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](http://www.typescriptlang.org/)
|
|
6
|
+
|
|
7
|
+
Route model binding for Express.js. If you've used Laravel, you know how useful this pattern is—automatically resolve route parameters to database models.
|
|
8
|
+
|
|
9
|
+
Works with Knex, Mongoose, TypeORM, Sequelize, and Prisma.
|
|
10
|
+
|
|
11
|
+
## Why?
|
|
12
|
+
|
|
13
|
+
Instead of this:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
app.get('/users/:id', async (req, res) => {
|
|
17
|
+
const user = await db('users').where('id', req.params.id).first();
|
|
18
|
+
if (!user) return res.status(404).json({ error: 'User not found' });
|
|
19
|
+
res.json(user);
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Write this:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
app.get('/users/:user', bindModel('user', 'users'), (req, res) => {
|
|
27
|
+
res.json(req.user);
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install express-model-binding
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then install your ORM:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install knex pg # Knex + Postgres
|
|
41
|
+
npm install mongoose # MongoDB
|
|
42
|
+
npm install typeorm # TypeORM
|
|
43
|
+
npm install sequelize # Sequelize
|
|
44
|
+
npm install @prisma/client # Prisma
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Setup
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import express from 'express';
|
|
51
|
+
import Knex from 'knex';
|
|
52
|
+
import { ModelBinder, KnexAdapter, bindModel } from 'express-model-binding';
|
|
53
|
+
|
|
54
|
+
const app = express();
|
|
55
|
+
const knex = Knex({ client: 'pg', connection: process.env.DATABASE_URL });
|
|
56
|
+
|
|
57
|
+
// Configure once at startup
|
|
58
|
+
ModelBinder.setAdapter(new KnexAdapter(knex));
|
|
59
|
+
|
|
60
|
+
// Use in routes
|
|
61
|
+
app.get('/users/:user', bindModel('user', 'users'), (req, res) => {
|
|
62
|
+
res.json(req.user);
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Adapters
|
|
67
|
+
|
|
68
|
+
**Knex**
|
|
69
|
+
```typescript
|
|
70
|
+
import { KnexAdapter } from 'express-model-binding';
|
|
71
|
+
ModelBinder.setAdapter(new KnexAdapter(knex));
|
|
72
|
+
app.get('/users/:user', bindModel('user', 'users'), handler);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Mongoose**
|
|
76
|
+
```typescript
|
|
77
|
+
import { MongooseAdapter } from 'express-model-binding';
|
|
78
|
+
ModelBinder.setAdapter(new MongooseAdapter());
|
|
79
|
+
app.get('/users/:user', bindModel('user', User), handler);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**TypeORM**
|
|
83
|
+
```typescript
|
|
84
|
+
import { TypeORMAdapter } from 'express-model-binding';
|
|
85
|
+
ModelBinder.setAdapter(new TypeORMAdapter(dataSource));
|
|
86
|
+
app.get('/users/:user', bindModel('user', User), handler);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Sequelize**
|
|
90
|
+
```typescript
|
|
91
|
+
import { SequelizeAdapter } from 'express-model-binding';
|
|
92
|
+
ModelBinder.setAdapter(new SequelizeAdapter(sequelize));
|
|
93
|
+
app.get('/users/:user', bindModel('user', User), handler);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Prisma**
|
|
97
|
+
```typescript
|
|
98
|
+
import { PrismaAdapter } from 'express-model-binding';
|
|
99
|
+
ModelBinder.setAdapter(new PrismaAdapter(prisma));
|
|
100
|
+
app.get('/users/:user', bindModel('user', 'user'), handler);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Middleware
|
|
104
|
+
|
|
105
|
+
**bindModel** — Basic binding
|
|
106
|
+
```typescript
|
|
107
|
+
app.get('/users/:user', bindModel('user', 'users'), handler);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**bindModels** — Multiple models
|
|
111
|
+
```typescript
|
|
112
|
+
app.get('/users/:user/posts/:post', bindModels({
|
|
113
|
+
user: { model: 'users' },
|
|
114
|
+
post: { model: 'posts' },
|
|
115
|
+
}), handler);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**bindOptional** — Don't throw if missing
|
|
119
|
+
```typescript
|
|
120
|
+
app.get('/users/:user', bindOptional('user', 'users'), handler);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**bindByKey** — Bind by slug, email, etc.
|
|
124
|
+
```typescript
|
|
125
|
+
app.get('/posts/:slug', bindByKey('slug', 'posts', 'slug'), handler);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**bindAs** — Custom request property name
|
|
129
|
+
```typescript
|
|
130
|
+
app.get('/profile/:id', bindAs('id', 'users', 'profile'), handler);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**bindCached** — With caching
|
|
134
|
+
```typescript
|
|
135
|
+
app.get('/users/:user', bindCached('user', 'users', 60000), handler);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**bindWithRelations** — Eager load relations
|
|
139
|
+
```typescript
|
|
140
|
+
app.get('/users/:user', bindWithRelations('user', 'users', ['posts']), handler);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Options
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
bindModel('user', 'users', {
|
|
147
|
+
key: 'slug', // Field to query (default: primary key)
|
|
148
|
+
optional: true, // Don't throw 404 if not found
|
|
149
|
+
select: ['id', 'name'], // Select specific fields
|
|
150
|
+
include: ['posts'], // Load relations
|
|
151
|
+
where: { active: true }, // Extra conditions
|
|
152
|
+
withTrashed: true, // Include soft-deleted
|
|
153
|
+
cache: true, // Enable caching
|
|
154
|
+
cacheTTL: 30000, // Cache duration (ms)
|
|
155
|
+
errorMessage: 'Not found',
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Error Handling
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { ModelNotFoundError } from 'express-model-binding';
|
|
163
|
+
|
|
164
|
+
app.use((err, req, res, next) => {
|
|
165
|
+
if (err instanceof ModelNotFoundError) {
|
|
166
|
+
return res.status(404).json({ error: err.message });
|
|
167
|
+
}
|
|
168
|
+
next(err);
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Utilities
|
|
173
|
+
|
|
174
|
+
**Transformers** — Convert parameter values
|
|
175
|
+
```typescript
|
|
176
|
+
import { toNumber, toLowerCase } from 'express-model-binding';
|
|
177
|
+
bindModel('user', 'users', { transformValue: toNumber });
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Validators** — Check format before querying
|
|
181
|
+
```typescript
|
|
182
|
+
import { isUUID, isObjectId } from 'express-model-binding';
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Debugging
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
ModelBinder.setDebug(true);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## API
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
ModelBinder.setAdapter(adapter) // Set ORM adapter
|
|
195
|
+
ModelBinder.getAdapter() // Get current adapter
|
|
196
|
+
ModelBinder.clearCache() // Clear binding cache
|
|
197
|
+
ModelBinder.reset() // Reset all state
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
MIT © [Sepehr Mohseni](https://github.com/sepehr-mohseni)
|
|
203
|
+
|
|
204
|
+
## Links
|
|
205
|
+
|
|
206
|
+
- [npm](https://www.npmjs.com/package/express-model-binding)
|
|
207
|
+
- [GitHub](https://github.com/sepehr-mohseni/express-model-binding)
|
|
208
|
+
- [Issues](https://github.com/sepehr-mohseni/express-model-binding/issues)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generic query builder type - adapters can narrow this to their specific type
|
|
5
|
+
*/
|
|
6
|
+
type QueryBuilder<T = unknown> = T;
|
|
7
|
+
/**
|
|
8
|
+
* Query modifier function type
|
|
9
|
+
*/
|
|
10
|
+
type QueryModifier<T = unknown> = (queryBuilder: QueryBuilder<T>) => QueryBuilder<T>;
|
|
11
|
+
/**
|
|
12
|
+
* Base adapter interface that all ORM adapters must implement
|
|
13
|
+
*/
|
|
14
|
+
interface IORMAdapter<TModel = unknown, TResult = unknown> {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
findByKey(model: TModel, key: string, value: unknown, options?: QueryOptions): Promise<TResult | null>;
|
|
17
|
+
getPrimaryKeyName(model: TModel): string;
|
|
18
|
+
isValidModel(model: unknown): model is TModel;
|
|
19
|
+
transformValue(model: TModel, key: string, value: string): unknown;
|
|
20
|
+
supportsSoftDeletes(model: TModel): boolean;
|
|
21
|
+
getModelMetadata?(model: TModel): ModelMetadata;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Query options for model lookups
|
|
25
|
+
*/
|
|
26
|
+
interface QueryOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Relations to eager load
|
|
29
|
+
*/
|
|
30
|
+
include?: string[] | Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Additional WHERE conditions
|
|
33
|
+
*/
|
|
34
|
+
where?: Record<string, unknown>;
|
|
35
|
+
/**
|
|
36
|
+
* Custom query modifier - receives ORM-specific query builder
|
|
37
|
+
*/
|
|
38
|
+
query?: QueryModifier;
|
|
39
|
+
/**
|
|
40
|
+
* Fields to select
|
|
41
|
+
*/
|
|
42
|
+
select?: string[];
|
|
43
|
+
/**
|
|
44
|
+
* Include soft-deleted records
|
|
45
|
+
*/
|
|
46
|
+
withTrashed?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Only return soft-deleted records
|
|
49
|
+
*/
|
|
50
|
+
onlyTrashed?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Enable result caching
|
|
53
|
+
*/
|
|
54
|
+
cache?: boolean | number;
|
|
55
|
+
/**
|
|
56
|
+
* Row locking for transactions
|
|
57
|
+
*/
|
|
58
|
+
lock?: 'forUpdate' | 'forShare';
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Binding middleware options
|
|
62
|
+
*/
|
|
63
|
+
interface BindOptions extends QueryOptions {
|
|
64
|
+
/**
|
|
65
|
+
* Field to search by (defaults to primary key)
|
|
66
|
+
*/
|
|
67
|
+
key?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Custom error when model not found
|
|
70
|
+
*/
|
|
71
|
+
onNotFound?: Error | ((paramName: string, paramValue: string) => Error);
|
|
72
|
+
/**
|
|
73
|
+
* Transform parameter value before querying
|
|
74
|
+
*/
|
|
75
|
+
transformValue?: (value: string) => unknown;
|
|
76
|
+
/**
|
|
77
|
+
* Property name for attaching model to request
|
|
78
|
+
*/
|
|
79
|
+
as?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Don't throw if model not found
|
|
82
|
+
*/
|
|
83
|
+
optional?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Custom 404 message
|
|
86
|
+
*/
|
|
87
|
+
errorMessage?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Validate loaded model
|
|
90
|
+
*/
|
|
91
|
+
validate?: (model: unknown, req: Request) => void | Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Enable caching
|
|
94
|
+
*/
|
|
95
|
+
cache?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Cache TTL in milliseconds
|
|
98
|
+
*/
|
|
99
|
+
cacheTTL?: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Multi-model binding configuration
|
|
103
|
+
*/
|
|
104
|
+
interface ModelBindingsConfig {
|
|
105
|
+
[paramName: string]: {
|
|
106
|
+
model: unknown;
|
|
107
|
+
options?: BindOptions;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Binding operation context
|
|
112
|
+
*/
|
|
113
|
+
interface BindingContext {
|
|
114
|
+
req: Request;
|
|
115
|
+
res: Response;
|
|
116
|
+
paramName: string;
|
|
117
|
+
paramValue: string;
|
|
118
|
+
model: unknown;
|
|
119
|
+
options: BindOptions;
|
|
120
|
+
adapter: IORMAdapter;
|
|
121
|
+
startTime: number;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Binding operation result
|
|
125
|
+
*/
|
|
126
|
+
interface BindingResult {
|
|
127
|
+
success: boolean;
|
|
128
|
+
model?: unknown;
|
|
129
|
+
error?: Error;
|
|
130
|
+
duration: number;
|
|
131
|
+
fromCache?: boolean;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Model metadata for debugging
|
|
135
|
+
*/
|
|
136
|
+
interface ModelMetadata {
|
|
137
|
+
name: string;
|
|
138
|
+
primaryKey: string;
|
|
139
|
+
tableName?: string;
|
|
140
|
+
relations?: string[];
|
|
141
|
+
softDeletes: boolean;
|
|
142
|
+
adapter?: string;
|
|
143
|
+
[key: string]: unknown;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Cache entry structure
|
|
147
|
+
*/
|
|
148
|
+
interface CacheEntry<T = unknown> {
|
|
149
|
+
value: T;
|
|
150
|
+
timestamp: number;
|
|
151
|
+
ttl: number;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Global configuration options
|
|
155
|
+
*/
|
|
156
|
+
interface ModelBindingGlobalConfig {
|
|
157
|
+
adapter?: IORMAdapter;
|
|
158
|
+
cache?: {
|
|
159
|
+
enabled: boolean;
|
|
160
|
+
ttl: number;
|
|
161
|
+
maxSize?: number;
|
|
162
|
+
};
|
|
163
|
+
debug?: boolean;
|
|
164
|
+
logger?: (message: string, context?: unknown) => void;
|
|
165
|
+
onError?: (error: Error, context: BindingContext) => void;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Express request with bound models
|
|
169
|
+
*/
|
|
170
|
+
interface TypedRequest<P = Record<string, string>, ResBody = unknown, ReqBody = unknown, ReqQuery = unknown, Models extends Record<string, unknown> = Record<string, unknown>> extends Request<P, ResBody, ReqBody, ReqQuery> {
|
|
171
|
+
[K: string]: unknown;
|
|
172
|
+
models?: Models;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Extract model type from binding config
|
|
176
|
+
*/
|
|
177
|
+
type ExtractModelType<T> = T extends {
|
|
178
|
+
model: infer M;
|
|
179
|
+
} ? M : never;
|
|
180
|
+
/**
|
|
181
|
+
* Typed request handler
|
|
182
|
+
*/
|
|
183
|
+
type TypedRequestHandler<Models extends Record<string, unknown> = Record<string, unknown>, P = Record<string, string>, ResBody = unknown, ReqBody = unknown, ReqQuery = unknown> = (req: TypedRequest<P, ResBody, ReqBody, ReqQuery, Models>, res: Response<ResBody>, next: NextFunction) => void | Promise<void>;
|
|
184
|
+
/**
|
|
185
|
+
* Middleware function type
|
|
186
|
+
*/
|
|
187
|
+
type MiddlewareFunction = RequestHandler;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Abstract base class providing common adapter functionality.
|
|
191
|
+
* Extend this class to implement ORM-specific adapters.
|
|
192
|
+
*
|
|
193
|
+
* @typeParam TModel - Model type accepted by this adapter
|
|
194
|
+
* @typeParam TResult - Result type returned by queries
|
|
195
|
+
* @typeParam TQueryBuilder - ORM-specific query builder type
|
|
196
|
+
*/
|
|
197
|
+
declare abstract class BaseAdapter<TModel = unknown, TResult = unknown, TQueryBuilder = unknown> implements IORMAdapter<TModel, TResult> {
|
|
198
|
+
abstract readonly name: string;
|
|
199
|
+
abstract findByKey(model: TModel, key: string, value: unknown, options?: QueryOptions): Promise<TResult | null>;
|
|
200
|
+
abstract getPrimaryKeyName(model: TModel): string;
|
|
201
|
+
abstract isValidModel(model: unknown): model is TModel;
|
|
202
|
+
transformValue(_model: TModel, _key: string, value: string): unknown;
|
|
203
|
+
supportsSoftDeletes(_model: TModel): boolean;
|
|
204
|
+
getModelMetadata(model: TModel): ModelMetadata;
|
|
205
|
+
protected validateModel(model: unknown): asserts model is TModel;
|
|
206
|
+
protected getModelName(model: TModel): string;
|
|
207
|
+
protected applySoftDeleteFilter(queryBuilder: TQueryBuilder, _options?: QueryOptions): TQueryBuilder;
|
|
208
|
+
protected applyIncludes(queryBuilder: TQueryBuilder, _includes?: string[] | Record<string, unknown>): TQueryBuilder;
|
|
209
|
+
protected applySelect(queryBuilder: TQueryBuilder, _select?: string[]): TQueryBuilder;
|
|
210
|
+
protected applyWhereConditions(queryBuilder: TQueryBuilder, _where?: Record<string, unknown>): TQueryBuilder;
|
|
211
|
+
protected applyCustomQuery(queryBuilder: TQueryBuilder, queryFn?: QueryModifier<TQueryBuilder>): TQueryBuilder;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export { type BindOptions as B, type CacheEntry as C, type ExtractModelType as E, type IORMAdapter as I, type ModelBindingsConfig as M, type QueryOptions as Q, type TypedRequest as T, type BindingResult as a, BaseAdapter as b, type BindingContext as c, type ModelMetadata as d, type ModelBindingGlobalConfig as e, type TypedRequestHandler as f, type MiddlewareFunction as g };
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generic query builder type - adapters can narrow this to their specific type
|
|
5
|
+
*/
|
|
6
|
+
type QueryBuilder<T = unknown> = T;
|
|
7
|
+
/**
|
|
8
|
+
* Query modifier function type
|
|
9
|
+
*/
|
|
10
|
+
type QueryModifier<T = unknown> = (queryBuilder: QueryBuilder<T>) => QueryBuilder<T>;
|
|
11
|
+
/**
|
|
12
|
+
* Base adapter interface that all ORM adapters must implement
|
|
13
|
+
*/
|
|
14
|
+
interface IORMAdapter<TModel = unknown, TResult = unknown> {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
findByKey(model: TModel, key: string, value: unknown, options?: QueryOptions): Promise<TResult | null>;
|
|
17
|
+
getPrimaryKeyName(model: TModel): string;
|
|
18
|
+
isValidModel(model: unknown): model is TModel;
|
|
19
|
+
transformValue(model: TModel, key: string, value: string): unknown;
|
|
20
|
+
supportsSoftDeletes(model: TModel): boolean;
|
|
21
|
+
getModelMetadata?(model: TModel): ModelMetadata;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Query options for model lookups
|
|
25
|
+
*/
|
|
26
|
+
interface QueryOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Relations to eager load
|
|
29
|
+
*/
|
|
30
|
+
include?: string[] | Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Additional WHERE conditions
|
|
33
|
+
*/
|
|
34
|
+
where?: Record<string, unknown>;
|
|
35
|
+
/**
|
|
36
|
+
* Custom query modifier - receives ORM-specific query builder
|
|
37
|
+
*/
|
|
38
|
+
query?: QueryModifier;
|
|
39
|
+
/**
|
|
40
|
+
* Fields to select
|
|
41
|
+
*/
|
|
42
|
+
select?: string[];
|
|
43
|
+
/**
|
|
44
|
+
* Include soft-deleted records
|
|
45
|
+
*/
|
|
46
|
+
withTrashed?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Only return soft-deleted records
|
|
49
|
+
*/
|
|
50
|
+
onlyTrashed?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Enable result caching
|
|
53
|
+
*/
|
|
54
|
+
cache?: boolean | number;
|
|
55
|
+
/**
|
|
56
|
+
* Row locking for transactions
|
|
57
|
+
*/
|
|
58
|
+
lock?: 'forUpdate' | 'forShare';
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Binding middleware options
|
|
62
|
+
*/
|
|
63
|
+
interface BindOptions extends QueryOptions {
|
|
64
|
+
/**
|
|
65
|
+
* Field to search by (defaults to primary key)
|
|
66
|
+
*/
|
|
67
|
+
key?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Custom error when model not found
|
|
70
|
+
*/
|
|
71
|
+
onNotFound?: Error | ((paramName: string, paramValue: string) => Error);
|
|
72
|
+
/**
|
|
73
|
+
* Transform parameter value before querying
|
|
74
|
+
*/
|
|
75
|
+
transformValue?: (value: string) => unknown;
|
|
76
|
+
/**
|
|
77
|
+
* Property name for attaching model to request
|
|
78
|
+
*/
|
|
79
|
+
as?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Don't throw if model not found
|
|
82
|
+
*/
|
|
83
|
+
optional?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Custom 404 message
|
|
86
|
+
*/
|
|
87
|
+
errorMessage?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Validate loaded model
|
|
90
|
+
*/
|
|
91
|
+
validate?: (model: unknown, req: Request) => void | Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Enable caching
|
|
94
|
+
*/
|
|
95
|
+
cache?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Cache TTL in milliseconds
|
|
98
|
+
*/
|
|
99
|
+
cacheTTL?: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Multi-model binding configuration
|
|
103
|
+
*/
|
|
104
|
+
interface ModelBindingsConfig {
|
|
105
|
+
[paramName: string]: {
|
|
106
|
+
model: unknown;
|
|
107
|
+
options?: BindOptions;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Binding operation context
|
|
112
|
+
*/
|
|
113
|
+
interface BindingContext {
|
|
114
|
+
req: Request;
|
|
115
|
+
res: Response;
|
|
116
|
+
paramName: string;
|
|
117
|
+
paramValue: string;
|
|
118
|
+
model: unknown;
|
|
119
|
+
options: BindOptions;
|
|
120
|
+
adapter: IORMAdapter;
|
|
121
|
+
startTime: number;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Binding operation result
|
|
125
|
+
*/
|
|
126
|
+
interface BindingResult {
|
|
127
|
+
success: boolean;
|
|
128
|
+
model?: unknown;
|
|
129
|
+
error?: Error;
|
|
130
|
+
duration: number;
|
|
131
|
+
fromCache?: boolean;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Model metadata for debugging
|
|
135
|
+
*/
|
|
136
|
+
interface ModelMetadata {
|
|
137
|
+
name: string;
|
|
138
|
+
primaryKey: string;
|
|
139
|
+
tableName?: string;
|
|
140
|
+
relations?: string[];
|
|
141
|
+
softDeletes: boolean;
|
|
142
|
+
adapter?: string;
|
|
143
|
+
[key: string]: unknown;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Cache entry structure
|
|
147
|
+
*/
|
|
148
|
+
interface CacheEntry<T = unknown> {
|
|
149
|
+
value: T;
|
|
150
|
+
timestamp: number;
|
|
151
|
+
ttl: number;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Global configuration options
|
|
155
|
+
*/
|
|
156
|
+
interface ModelBindingGlobalConfig {
|
|
157
|
+
adapter?: IORMAdapter;
|
|
158
|
+
cache?: {
|
|
159
|
+
enabled: boolean;
|
|
160
|
+
ttl: number;
|
|
161
|
+
maxSize?: number;
|
|
162
|
+
};
|
|
163
|
+
debug?: boolean;
|
|
164
|
+
logger?: (message: string, context?: unknown) => void;
|
|
165
|
+
onError?: (error: Error, context: BindingContext) => void;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Express request with bound models
|
|
169
|
+
*/
|
|
170
|
+
interface TypedRequest<P = Record<string, string>, ResBody = unknown, ReqBody = unknown, ReqQuery = unknown, Models extends Record<string, unknown> = Record<string, unknown>> extends Request<P, ResBody, ReqBody, ReqQuery> {
|
|
171
|
+
[K: string]: unknown;
|
|
172
|
+
models?: Models;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Extract model type from binding config
|
|
176
|
+
*/
|
|
177
|
+
type ExtractModelType<T> = T extends {
|
|
178
|
+
model: infer M;
|
|
179
|
+
} ? M : never;
|
|
180
|
+
/**
|
|
181
|
+
* Typed request handler
|
|
182
|
+
*/
|
|
183
|
+
type TypedRequestHandler<Models extends Record<string, unknown> = Record<string, unknown>, P = Record<string, string>, ResBody = unknown, ReqBody = unknown, ReqQuery = unknown> = (req: TypedRequest<P, ResBody, ReqBody, ReqQuery, Models>, res: Response<ResBody>, next: NextFunction) => void | Promise<void>;
|
|
184
|
+
/**
|
|
185
|
+
* Middleware function type
|
|
186
|
+
*/
|
|
187
|
+
type MiddlewareFunction = RequestHandler;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Abstract base class providing common adapter functionality.
|
|
191
|
+
* Extend this class to implement ORM-specific adapters.
|
|
192
|
+
*
|
|
193
|
+
* @typeParam TModel - Model type accepted by this adapter
|
|
194
|
+
* @typeParam TResult - Result type returned by queries
|
|
195
|
+
* @typeParam TQueryBuilder - ORM-specific query builder type
|
|
196
|
+
*/
|
|
197
|
+
declare abstract class BaseAdapter<TModel = unknown, TResult = unknown, TQueryBuilder = unknown> implements IORMAdapter<TModel, TResult> {
|
|
198
|
+
abstract readonly name: string;
|
|
199
|
+
abstract findByKey(model: TModel, key: string, value: unknown, options?: QueryOptions): Promise<TResult | null>;
|
|
200
|
+
abstract getPrimaryKeyName(model: TModel): string;
|
|
201
|
+
abstract isValidModel(model: unknown): model is TModel;
|
|
202
|
+
transformValue(_model: TModel, _key: string, value: string): unknown;
|
|
203
|
+
supportsSoftDeletes(_model: TModel): boolean;
|
|
204
|
+
getModelMetadata(model: TModel): ModelMetadata;
|
|
205
|
+
protected validateModel(model: unknown): asserts model is TModel;
|
|
206
|
+
protected getModelName(model: TModel): string;
|
|
207
|
+
protected applySoftDeleteFilter(queryBuilder: TQueryBuilder, _options?: QueryOptions): TQueryBuilder;
|
|
208
|
+
protected applyIncludes(queryBuilder: TQueryBuilder, _includes?: string[] | Record<string, unknown>): TQueryBuilder;
|
|
209
|
+
protected applySelect(queryBuilder: TQueryBuilder, _select?: string[]): TQueryBuilder;
|
|
210
|
+
protected applyWhereConditions(queryBuilder: TQueryBuilder, _where?: Record<string, unknown>): TQueryBuilder;
|
|
211
|
+
protected applyCustomQuery(queryBuilder: TQueryBuilder, queryFn?: QueryModifier<TQueryBuilder>): TQueryBuilder;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export { type BindOptions as B, type CacheEntry as C, type ExtractModelType as E, type IORMAdapter as I, type ModelBindingsConfig as M, type QueryOptions as Q, type TypedRequest as T, type BindingResult as a, BaseAdapter as b, type BindingContext as c, type ModelMetadata as d, type ModelBindingGlobalConfig as e, type TypedRequestHandler as f, type MiddlewareFunction as g };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Knex } from 'knex';
|
|
2
|
+
import { b as BaseAdapter, Q as QueryOptions, d as ModelMetadata } from '../BaseAdapter-BjvLQijd.mjs';
|
|
3
|
+
import 'express';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Model definition for Knex tables with soft delete support
|
|
7
|
+
*/
|
|
8
|
+
interface KnexModel {
|
|
9
|
+
tableName: string;
|
|
10
|
+
primaryKey?: string;
|
|
11
|
+
softDeleteColumn?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Union type for Knex model inputs
|
|
15
|
+
*/
|
|
16
|
+
type KnexModelInput = string | KnexModel;
|
|
17
|
+
/**
|
|
18
|
+
* Adapter for Knex query builder supporting PostgreSQL, MySQL, SQLite, MSSQL, Oracle
|
|
19
|
+
*/
|
|
20
|
+
declare class KnexAdapter extends BaseAdapter<KnexModelInput, Record<string, unknown>, Knex.QueryBuilder> {
|
|
21
|
+
private knex;
|
|
22
|
+
readonly name = "knex";
|
|
23
|
+
constructor(knex: Knex);
|
|
24
|
+
getKnex(): Knex;
|
|
25
|
+
findByKey(model: KnexModelInput, key: string, value: unknown, options?: QueryOptions): Promise<Record<string, unknown> | null>;
|
|
26
|
+
getPrimaryKeyName(model: KnexModelInput): string;
|
|
27
|
+
isValidModel(model: unknown): model is KnexModelInput;
|
|
28
|
+
transformValue(model: KnexModelInput, key: string, value: string): unknown;
|
|
29
|
+
supportsSoftDeletes(model: KnexModelInput): boolean;
|
|
30
|
+
getModelMetadata(model: KnexModelInput): ModelMetadata;
|
|
31
|
+
private getTableName;
|
|
32
|
+
private getSoftDeleteColumn;
|
|
33
|
+
protected applyWhereConditions(query: Knex.QueryBuilder, where: Record<string, unknown>): Knex.QueryBuilder;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create a Knex model definition
|
|
37
|
+
*/
|
|
38
|
+
declare function defineKnexModel(config: {
|
|
39
|
+
tableName: string;
|
|
40
|
+
primaryKey?: string;
|
|
41
|
+
softDeleteColumn?: string;
|
|
42
|
+
}): KnexModel;
|
|
43
|
+
|
|
44
|
+
export { KnexAdapter, type KnexModel, type KnexModelInput, defineKnexModel };
|