@simplysm/orm-common 13.0.42 → 13.0.43
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 +870 -168
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -21,65 +21,15 @@ pnpm add @simplysm/orm-common
|
|
|
21
21
|
|
|
22
22
|
## Main Modules
|
|
23
23
|
|
|
24
|
-
###
|
|
24
|
+
### Core
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- **[Table(name)](docs/schema.md#table-definition)** - Table builder factory function
|
|
29
|
-
- **[View(name)](docs/schema.md#view-definition)** - View builder factory function
|
|
30
|
-
- **[Procedure(name)](docs/schema.md#procedure-definition)** - Procedure builder factory function
|
|
31
|
-
- **[Column Types](docs/schema.md#column-types)** - `c.int()`, `c.varchar()`, `c.datetime()`, etc.
|
|
32
|
-
- **[Column Options](docs/schema.md#column-options)** - `.nullable()`, `.autoIncrement()`, `.default()`, `.description()`
|
|
33
|
-
- **[Relationships](docs/schema.md#relationship-definition)** - `r.foreignKey()`, `r.foreignKeyTarget()`, `r.relationKey()`, `r.relationKeyTarget()`
|
|
34
|
-
- **[DbContext](docs/schema.md#dbcontext-configuration)** - Database context class for connection, transactions, and migrations
|
|
35
|
-
- **[Type Inference](docs/schema.md#type-inference)** - `$infer`, `$inferInsert`, `$inferUpdate`
|
|
36
|
-
|
|
37
|
-
### Query Execution
|
|
38
|
-
|
|
39
|
-
See [docs/queries.md](docs/queries.md) for full documentation.
|
|
40
|
-
|
|
41
|
-
- **[Connection & Transactions](docs/queries.md#connection-and-transactions)** - `db.connect()`, `db.connectWithoutTransaction()`, `db.trans()`
|
|
42
|
-
- **[SELECT Queries](docs/queries.md#select-queries)** - `.where()`, `.select()`, `.distinct()`, `.orderBy()`, `.single()`, `.first()`, `.result()`, `.count()`, `.exists()`
|
|
43
|
-
- **[JOIN Queries](docs/queries.md#join-queries)** - `.join()`, `.joinSingle()`, `.include()`
|
|
44
|
-
- **[Grouping & Aggregation](docs/queries.md#grouping-and-aggregation)** - `.groupBy()`, `.having()`
|
|
45
|
-
- **[Pagination](docs/queries.md#pagination)** - `.top()`, `.limit()`
|
|
46
|
-
- **[Text Search](docs/queries.md#text-search)** - `.search()` with structured search syntax
|
|
47
|
-
- **[UNION](docs/queries.md#union)** - `Queryable.union()`
|
|
48
|
-
- **[Subquery Wrapping](docs/queries.md#subquery-wrapping-wrap)** - `.wrap()`
|
|
49
|
-
- **[Recursive CTE](docs/queries.md#recursive-cte-recursive)** - `.recursive()`
|
|
50
|
-
- **[INSERT](docs/queries.md#insert)** - `.insert()`, `.insertIfNotExists()`, `.insertInto()`
|
|
51
|
-
- **[UPDATE](docs/queries.md#update)** - `.update()`
|
|
52
|
-
- **[DELETE](docs/queries.md#delete)** - `.delete()`
|
|
53
|
-
- **[UPSERT](docs/queries.md#upsert)** - `.upsert()`
|
|
54
|
-
- **[Row Locking](docs/queries.md#row-locking-for-update)** - `.lock()` (FOR UPDATE)
|
|
55
|
-
- **[DDL Operations](docs/queries.md#ddl-operations)** - `db.initialize()`, `db.addColumn()`, `db.modifyColumn()`, etc.
|
|
56
|
-
- **[Query Builder](docs/queries.md#query-builder-sql-generation)** - `createQueryBuilder()` for converting QueryDef to SQL
|
|
57
|
-
- **[Error Handling](docs/queries.md#error-handling)** - `DbTransactionError`, `DbErrorCode`
|
|
58
|
-
|
|
59
|
-
### SQL Expressions
|
|
60
|
-
|
|
61
|
-
See [docs/expressions.md](docs/expressions.md) for full documentation.
|
|
62
|
-
|
|
63
|
-
- **[Comparison Expressions](docs/expressions.md#comparison-expressions-where)** - `expr.eq()`, `expr.gt()`, `expr.lt()`, `expr.between()`, `expr.in()`, `expr.like()`, `expr.regexp()`, `expr.exists()`
|
|
64
|
-
- **[Logical Expressions](docs/expressions.md#logical-expressions-where)** - `expr.and()`, `expr.or()`, `expr.not()`
|
|
65
|
-
- **[String Expressions](docs/expressions.md#string-expressions)** - `expr.concat()`, `expr.trim()`, `expr.substring()`, `expr.upper()`, `expr.lower()`, `expr.length()`
|
|
66
|
-
- **[Numeric Expressions](docs/expressions.md#numeric-expressions)** - `expr.abs()`, `expr.round()`, `expr.ceil()`, `expr.floor()`
|
|
67
|
-
- **[Date Expressions](docs/expressions.md#date-expressions)** - `expr.year()`, `expr.month()`, `expr.day()`, `expr.dateDiff()`, `expr.dateAdd()`, `expr.formatDate()`
|
|
68
|
-
- **[Conditional Expressions](docs/expressions.md#conditional-expressions)** - `expr.ifNull()`, `expr.nullIf()`, `expr.if()`, `expr.switch()`
|
|
69
|
-
- **[Aggregate Expressions](docs/expressions.md#aggregate-expressions)** - `expr.count()`, `expr.sum()`, `expr.avg()`, `expr.max()`, `expr.min()`, `expr.greatest()`, `expr.least()`
|
|
70
|
-
- **[Window Functions](docs/expressions.md#window-functions)** - `expr.rowNumber()`, `expr.rank()`, `expr.denseRank()`, `expr.ntile()`, `expr.lag()`, `expr.lead()`, `expr.firstValue()`, `expr.lastValue()`, `expr.sumOver()`, `expr.avgOver()`, `expr.countOver()`, `expr.minOver()`, `expr.maxOver()`
|
|
71
|
-
- **[Other Expressions](docs/expressions.md#other-expressions)** - `expr.val()`, `expr.raw()`, `expr.cast()`, `expr.subquery()`, `expr.random()`
|
|
72
|
-
|
|
73
|
-
## DbContext API
|
|
74
|
-
|
|
75
|
-
### Functional API (Recommended)
|
|
26
|
+
#### `defineDbContext(config)`
|
|
76
27
|
|
|
77
|
-
|
|
28
|
+
Creates a `DbContextDef` schema blueprint without any runtime state.
|
|
78
29
|
|
|
79
30
|
```typescript
|
|
80
|
-
import { defineDbContext
|
|
31
|
+
import { defineDbContext } from "@simplysm/orm-common";
|
|
81
32
|
|
|
82
|
-
// Step 1: Define DbContext schema
|
|
83
33
|
const MyDbDef = defineDbContext({
|
|
84
34
|
tables: { user: User, post: Post },
|
|
85
35
|
views: { activeUsers: ActiveUsers },
|
|
@@ -98,34 +48,33 @@ const MyDbDef = defineDbContext({
|
|
|
98
48
|
},
|
|
99
49
|
],
|
|
100
50
|
});
|
|
101
|
-
|
|
102
|
-
// Step 2: Create instance with executor
|
|
103
|
-
const db = createDbContext(MyDbDef, executor, { database: "mydb" });
|
|
104
|
-
|
|
105
|
-
// Use queryable accessors
|
|
106
|
-
await db.connect(async () => {
|
|
107
|
-
const users = await db.user().result();
|
|
108
|
-
const posts = await db.post().result();
|
|
109
|
-
});
|
|
110
51
|
```
|
|
111
52
|
|
|
112
|
-
#### `defineDbContext(config)`
|
|
113
|
-
|
|
114
|
-
Creates a `DbContextDef` schema blueprint without any runtime state.
|
|
115
|
-
|
|
116
53
|
| Parameter | Type | Description |
|
|
117
54
|
|-----------|------|-------------|
|
|
118
55
|
| `config.tables` | `Record<string, TableBuilder>` | Table builders |
|
|
119
|
-
| `config.views` | `Record<string, ViewBuilder>` | View builders |
|
|
120
|
-
| `config.procedures` | `Record<string, ProcedureBuilder>` | Procedure builders |
|
|
121
|
-
| `config.migrations` | `Migration[]` | Migration list |
|
|
56
|
+
| `config.views` | `Record<string, ViewBuilder>` | View builders (optional) |
|
|
57
|
+
| `config.procedures` | `Record<string, ProcedureBuilder>` | Procedure builders (optional) |
|
|
58
|
+
| `config.migrations` | `Migration[]` | Migration list (optional) |
|
|
122
59
|
|
|
123
60
|
Returns `DbContextDef<TTables, TViews, TProcedures>`.
|
|
124
61
|
|
|
62
|
+
Note: `defineDbContext` automatically adds an internal `_Migration` system table to `tables` to track applied migrations.
|
|
63
|
+
|
|
125
64
|
#### `createDbContext(def, executor, opt)`
|
|
126
65
|
|
|
127
66
|
Creates a fully functional `DbContextInstance` from a `DbContextDef`.
|
|
128
67
|
|
|
68
|
+
```typescript
|
|
69
|
+
import { createDbContext } from "@simplysm/orm-common";
|
|
70
|
+
|
|
71
|
+
const db = createDbContext(MyDbDef, executor, { database: "mydb" });
|
|
72
|
+
|
|
73
|
+
await db.connect(async () => {
|
|
74
|
+
const users = await db.user().result();
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
129
78
|
| Parameter | Type | Description |
|
|
130
79
|
|-----------|------|-------------|
|
|
131
80
|
| `def` | `DbContextDef` | Schema definition from `defineDbContext()` |
|
|
@@ -146,87 +95,792 @@ Returns `DbContextInstance<TDef>`.
|
|
|
146
95
|
| `DbContextDdlMethods` | Interface for all DDL methods and QueryDef generator methods |
|
|
147
96
|
| `DbContextStatus` | `"ready" \| "connect" \| "transact"` — current connection status |
|
|
148
97
|
|
|
149
|
-
|
|
98
|
+
#### `DbTransactionError`
|
|
150
99
|
|
|
151
|
-
|
|
100
|
+
Database transaction error class. Wraps DBMS-native errors with a standardized `DbErrorCode`.
|
|
152
101
|
|
|
153
102
|
```typescript
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
103
|
+
import { DbTransactionError, DbErrorCode } from "@simplysm/orm-common";
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await executor.rollbackTransaction();
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (err instanceof DbTransactionError) {
|
|
109
|
+
if (err.code === DbErrorCode.NO_ACTIVE_TRANSACTION) {
|
|
110
|
+
return; // Already rolled back, ignore
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
164
115
|
```
|
|
165
116
|
|
|
166
|
-
|
|
117
|
+
| `DbErrorCode` value | Description |
|
|
118
|
+
|---------------------|-------------|
|
|
119
|
+
| `NO_ACTIVE_TRANSACTION` | No active transaction (rollback when none exists) |
|
|
120
|
+
| `TRANSACTION_ALREADY_STARTED` | Transaction already started |
|
|
121
|
+
| `DEADLOCK` | Deadlock detected |
|
|
122
|
+
| `LOCK_TIMEOUT` | Lock wait timeout |
|
|
123
|
+
|
|
124
|
+
`DbTransactionError` properties:
|
|
125
|
+
|
|
126
|
+
| Property | Type | Description |
|
|
127
|
+
|----------|------|-------------|
|
|
128
|
+
| `code` | `DbErrorCode` | Standardized error code |
|
|
129
|
+
| `message` | `string` | Error message |
|
|
130
|
+
| `originalError` | `unknown?` | Original DBMS error (for debugging) |
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### DbContext Instance API
|
|
167
135
|
|
|
168
|
-
|
|
136
|
+
The `DbContextInstance` returned by `createDbContext` exposes the following methods:
|
|
169
137
|
|
|
170
|
-
|
|
138
|
+
#### Connection Management
|
|
139
|
+
|
|
140
|
+
| Method | Description |
|
|
141
|
+
|--------|-------------|
|
|
142
|
+
| `connect(fn, isolationLevel?)` | Open connection → begin transaction → run callback → commit → close. Rolls back on error. |
|
|
143
|
+
| `connectWithoutTransaction(fn)` | Open connection → run callback → close. No transaction. Use for DDL or read-only operations. |
|
|
144
|
+
| `trans(fn, isolationLevel?)` | Start a transaction within an already-connected context. Use inside `connectWithoutTransaction` when partial transactions are needed. |
|
|
171
145
|
|
|
172
146
|
```typescript
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
147
|
+
// Default: with transaction
|
|
148
|
+
await db.connect(async () => {
|
|
149
|
+
await db.user().insert([{ name: "Alice" }]);
|
|
150
|
+
await db.post().insert([{ title: "Hello", userId: 1 }]);
|
|
151
|
+
});
|
|
178
152
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
153
|
+
// Without transaction (e.g., for DDL)
|
|
154
|
+
await db.connectWithoutTransaction(async () => {
|
|
155
|
+
await db.createTable(User);
|
|
156
|
+
});
|
|
183
157
|
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
],
|
|
158
|
+
// Nested transaction inside connectWithoutTransaction
|
|
159
|
+
await db.connectWithoutTransaction(async () => {
|
|
160
|
+
const report = await db.report().result();
|
|
161
|
+
await db.trans(async () => {
|
|
162
|
+
await db.log().insert([{ action: "read" }]);
|
|
163
|
+
});
|
|
191
164
|
});
|
|
192
165
|
```
|
|
193
166
|
|
|
194
|
-
|
|
167
|
+
#### Queryable / Executable Accessors
|
|
168
|
+
|
|
169
|
+
Each table/view/procedure defined in `defineDbContext` becomes an accessor method on the instance:
|
|
195
170
|
|
|
196
171
|
```typescript
|
|
197
|
-
//
|
|
198
|
-
|
|
172
|
+
// Tables and views → returns () => Queryable
|
|
173
|
+
db.user() // Queryable<UserData, typeof User>
|
|
174
|
+
db.post() // Queryable<PostData, typeof Post>
|
|
199
175
|
|
|
200
|
-
//
|
|
201
|
-
|
|
176
|
+
// Procedures → returns () => Executable
|
|
177
|
+
db.getUserById() // Executable<Params, Returns>
|
|
202
178
|
```
|
|
203
179
|
|
|
204
|
-
|
|
180
|
+
#### `initialize(options?)`
|
|
181
|
+
|
|
182
|
+
Applies all pending migrations in order.
|
|
205
183
|
|
|
206
184
|
```typescript
|
|
207
|
-
|
|
208
|
-
await db.
|
|
209
|
-
const users = await db.user().result(); // Same syntax
|
|
185
|
+
await db.connectWithoutTransaction(async () => {
|
|
186
|
+
await db.initialize();
|
|
210
187
|
});
|
|
211
188
|
```
|
|
212
189
|
|
|
213
|
-
|
|
190
|
+
| Option | Type | Description |
|
|
191
|
+
|--------|------|-------------|
|
|
192
|
+
| `options.dbs` | `string[]?` | Restrict migration to specific databases |
|
|
193
|
+
| `options.force` | `boolean?` | Re-apply all migrations even if already applied |
|
|
194
|
+
|
|
195
|
+
#### DDL Methods
|
|
196
|
+
|
|
197
|
+
All DDL methods are available on the `DbContextInstance`. DDL cannot be called inside a `connect()` (transact) context — use `connectWithoutTransaction` instead.
|
|
198
|
+
|
|
199
|
+
| Method | Description |
|
|
200
|
+
|--------|-------------|
|
|
201
|
+
| `createTable(table)` | Create a table based on `TableBuilder` |
|
|
202
|
+
| `dropTable(table)` | Drop a table |
|
|
203
|
+
| `renameTable(table, newName)` | Rename a table |
|
|
204
|
+
| `createView(view)` | Create a view |
|
|
205
|
+
| `dropView(view)` | Drop a view |
|
|
206
|
+
| `createProc(procedure)` | Create a stored procedure |
|
|
207
|
+
| `dropProc(procedure)` | Drop a stored procedure |
|
|
208
|
+
| `addColumn(table, columnName, column)` | Add a column |
|
|
209
|
+
| `dropColumn(table, column)` | Drop a column |
|
|
210
|
+
| `modifyColumn(table, columnName, column)` | Modify a column definition |
|
|
211
|
+
| `renameColumn(table, column, newName)` | Rename a column |
|
|
212
|
+
| `addPk(table, columns)` | Add a primary key |
|
|
213
|
+
| `dropPk(table)` | Drop the primary key |
|
|
214
|
+
| `addFk(table, relationName, relationDef)` | Add a foreign key |
|
|
215
|
+
| `dropFk(table, relationName)` | Drop a foreign key |
|
|
216
|
+
| `addIdx(table, indexBuilder)` | Add an index |
|
|
217
|
+
| `dropIdx(table, columns)` | Drop an index |
|
|
218
|
+
| `clearSchema(params)` | Drop all objects in a schema |
|
|
219
|
+
| `schemaExists(database, schema?)` | Check if a schema/database exists |
|
|
220
|
+
| `truncate(table)` | Truncate a table |
|
|
221
|
+
| `switchFk(table, "on" \| "off")` | Toggle FK constraint checking |
|
|
222
|
+
|
|
223
|
+
DDL `QueryDef` generator equivalents (return `QueryDef` without executing):
|
|
224
|
+
|
|
225
|
+
| Method | Description |
|
|
226
|
+
|--------|-------------|
|
|
227
|
+
| `getCreateTableQueryDef(table)` | |
|
|
228
|
+
| `getCreateViewQueryDef(view)` | |
|
|
229
|
+
| `getCreateProcQueryDef(procedure)` | |
|
|
230
|
+
| `getCreateObjectQueryDef(builder)` | Unified for table, view, or procedure |
|
|
231
|
+
| `getDropTableQueryDef(table)` | |
|
|
232
|
+
| `getRenameTableQueryDef(table, newName)` | |
|
|
233
|
+
| `getDropViewQueryDef(view)` | |
|
|
234
|
+
| `getDropProcQueryDef(procedure)` | |
|
|
235
|
+
| `getAddColumnQueryDef(table, columnName, column)` | |
|
|
236
|
+
| `getDropColumnQueryDef(table, column)` | |
|
|
237
|
+
| `getModifyColumnQueryDef(table, columnName, column)` | |
|
|
238
|
+
| `getRenameColumnQueryDef(table, column, newName)` | |
|
|
239
|
+
| `getAddPkQueryDef(table, columns)` | |
|
|
240
|
+
| `getDropPkQueryDef(table)` | |
|
|
241
|
+
| `getAddFkQueryDef(table, relationName, relationDef)` | |
|
|
242
|
+
| `getAddIdxQueryDef(table, indexBuilder)` | |
|
|
243
|
+
| `getDropFkQueryDef(table, relationName)` | |
|
|
244
|
+
| `getDropIdxQueryDef(table, columns)` | |
|
|
245
|
+
| `getClearSchemaQueryDef(params)` | |
|
|
246
|
+
| `getSchemaExistsQueryDef(database, schema?)` | |
|
|
247
|
+
| `getTruncateQueryDef(table)` | |
|
|
248
|
+
| `getSwitchFkQueryDef(table, switch_)` | |
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
### Queryable / Executable
|
|
253
|
+
|
|
254
|
+
#### `Queryable<TData, TFrom>`
|
|
255
|
+
|
|
256
|
+
The main query builder class. All methods are immutable and return a new `Queryable`.
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import { Queryable } from "@simplysm/orm-common";
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Query building methods (return `Queryable`):**
|
|
263
|
+
|
|
264
|
+
| Method | SQL | Notes |
|
|
265
|
+
|--------|-----|-------|
|
|
266
|
+
| `.select(fn)` | SELECT | Map columns to new shape |
|
|
267
|
+
| `.distinct()` | DISTINCT | Remove duplicate rows |
|
|
268
|
+
| `.lock()` | FOR UPDATE | Row-level exclusive lock (must be inside `connect`) |
|
|
269
|
+
| `.top(count)` | TOP / LIMIT | First N rows |
|
|
270
|
+
| `.limit(skip, take)` | OFFSET / FETCH | Pagination — requires prior `orderBy()` |
|
|
271
|
+
| `.orderBy(fn, dir?)` | ORDER BY | Add sort; multiple calls chain in order |
|
|
272
|
+
| `.where(predicate)` | WHERE | Add condition; multiple calls AND together |
|
|
273
|
+
| `.search(fn, searchText)` | WHERE LIKE | Multi-column text search with special syntax |
|
|
274
|
+
| `.groupBy(fn)` | GROUP BY | Group rows |
|
|
275
|
+
| `.having(predicate)` | HAVING | Filter groups |
|
|
276
|
+
| `.join(as, fwd)` | LEFT JOIN | 1:N relationship → result as array property |
|
|
277
|
+
| `.joinSingle(as, fwd)` | LEFT JOIN | N:1 / 1:1 → result as single object property |
|
|
278
|
+
| `.include(fn)` | LEFT JOIN | Auto-join using `TableBuilder` relation definitions |
|
|
279
|
+
| `.wrap()` | Subquery | Wrap current query as subquery |
|
|
280
|
+
| `.recursive(fwd)` | WITH RECURSIVE | Recursive CTE for hierarchical data |
|
|
281
|
+
|
|
282
|
+
**Static method:**
|
|
283
|
+
|
|
284
|
+
| Method | Description |
|
|
285
|
+
|--------|-------------|
|
|
286
|
+
| `Queryable.union(...queries)` | UNION of 2+ queryables (deduplication) |
|
|
287
|
+
|
|
288
|
+
**Execution methods:**
|
|
289
|
+
|
|
290
|
+
| Method | Returns | Description |
|
|
291
|
+
|--------|---------|-------------|
|
|
292
|
+
| `.result()` | `Promise<TData[]>` | Execute SELECT, return all rows |
|
|
293
|
+
| `.single()` | `Promise<TData \| undefined>` | Return one row; throws if more than one |
|
|
294
|
+
| `.first()` | `Promise<TData \| undefined>` | Return first row only |
|
|
295
|
+
| `.count(fwd?)` | `Promise<number>` | Count rows; cannot use after `distinct()`/`groupBy()` without `wrap()` |
|
|
296
|
+
| `.exists()` | `Promise<boolean>` | Check if any rows match |
|
|
297
|
+
| `.insert(records, outputColumns?)` | `Promise<void \| Pick[]>` | INSERT; returns output if columns specified; auto-chunks at 1000 |
|
|
298
|
+
| `.insertIfNotExists(record, outputColumns?)` | `Promise<void \| Pick>` | INSERT if WHERE condition not matched |
|
|
299
|
+
| `.insertInto(targetTable, outputColumns?)` | `Promise<void \| Pick[]>` | INSERT INTO ... SELECT |
|
|
300
|
+
| `.update(recordFwd, outputColumns?)` | `Promise<void \| Pick[]>` | UPDATE |
|
|
301
|
+
| `.delete(outputColumns?)` | `Promise<void \| Pick[]>` | DELETE |
|
|
302
|
+
| `.upsert(updateFwd, insertFwd?, outputColumns?)` | `Promise<void \| Pick[]>` | UPDATE or INSERT |
|
|
303
|
+
| `.switchFk("on" \| "off")` | `Promise<void>` | Toggle FK for this table |
|
|
304
|
+
|
|
305
|
+
**QueryDef generator methods (for custom execution):**
|
|
306
|
+
|
|
307
|
+
| Method | Description |
|
|
308
|
+
|--------|-------------|
|
|
309
|
+
| `.getSelectQueryDef()` | Build SELECT `QueryDef` |
|
|
310
|
+
| `.getInsertQueryDef(records, outputColumns?)` | Build INSERT `QueryDef` |
|
|
311
|
+
| `.getInsertIfNotExistsQueryDef(record, outputColumns?)` | Build INSERT IF NOT EXISTS `QueryDef` |
|
|
312
|
+
| `.getInsertIntoQueryDef(targetTable, outputColumns?)` | Build INSERT INTO SELECT `QueryDef` |
|
|
313
|
+
| `.getUpdateQueryDef(recordFwd, outputColumns?)` | Build UPDATE `QueryDef` |
|
|
314
|
+
| `.getDeleteQueryDef(outputColumns?)` | Build DELETE `QueryDef` |
|
|
315
|
+
| `.getUpsertQueryDef(updateFwd, insertFwd, outputColumns?)` | Build UPSERT `QueryDef` |
|
|
316
|
+
| `.getResultMeta(outputColumns?)` | Build `ResultMeta` for result parsing |
|
|
317
|
+
|
|
318
|
+
**Examples:**
|
|
214
319
|
|
|
215
320
|
```typescript
|
|
216
|
-
//
|
|
217
|
-
|
|
321
|
+
// SELECT with WHERE and ORDER BY
|
|
322
|
+
const users = await db.user()
|
|
323
|
+
.where((u) => [expr.eq(u.isActive, true)])
|
|
324
|
+
.orderBy((u) => u.name)
|
|
325
|
+
.result();
|
|
326
|
+
|
|
327
|
+
// JOIN (1:N)
|
|
328
|
+
const usersWithPosts = await db.user()
|
|
329
|
+
.join("posts", (qr, u) =>
|
|
330
|
+
qr.from(Post).where((p) => [expr.eq(p.userId, u.id)])
|
|
331
|
+
)
|
|
332
|
+
.result();
|
|
333
|
+
// Result: { id, name, posts?: Post[] }
|
|
334
|
+
|
|
335
|
+
// INCLUDE (using relation definitions)
|
|
336
|
+
const posts = await db.post()
|
|
337
|
+
.include((p) => p.author)
|
|
338
|
+
.result();
|
|
339
|
+
|
|
340
|
+
// Pagination
|
|
341
|
+
const page = await db.user()
|
|
342
|
+
.orderBy((u) => u.id)
|
|
343
|
+
.limit(0, 20)
|
|
344
|
+
.result();
|
|
345
|
+
|
|
346
|
+
// INSERT and get returned ID
|
|
347
|
+
const [inserted] = await db.user().insert(
|
|
348
|
+
[{ name: "Alice" }],
|
|
349
|
+
["id"],
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
// UPSERT
|
|
353
|
+
await db.user()
|
|
354
|
+
.where((u) => [expr.eq(u.email, "alice@example.com")])
|
|
355
|
+
.upsert(
|
|
356
|
+
() => ({ loginCount: expr.val("number", 1) }),
|
|
357
|
+
(update) => ({ ...update, email: expr.val("string", "alice@example.com") }),
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Recursive CTE (hierarchical query)
|
|
361
|
+
const tree = await db.employee()
|
|
362
|
+
.where((e) => [expr.null(e.managerId)])
|
|
363
|
+
.recursive((cte) =>
|
|
364
|
+
cte.from(Employee)
|
|
365
|
+
.where((e) => [expr.eq(e.managerId, e.self[0].id)])
|
|
366
|
+
)
|
|
367
|
+
.result();
|
|
368
|
+
```
|
|
218
369
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
370
|
+
#### `Executable<TParams, TReturns>`
|
|
371
|
+
|
|
372
|
+
Wrapper class for stored procedure execution.
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import { Executable } from "@simplysm/orm-common";
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
| Method | Returns | Description |
|
|
379
|
+
|--------|---------|-------------|
|
|
380
|
+
| `.execute(params)` | `Promise<InferColumnExprs<TReturns>[][]>` | Execute the stored procedure |
|
|
381
|
+
| `.getExecProcQueryDef(params?)` | `QueryDef` | Build EXEC PROC `QueryDef` |
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
const result = await db.getUserById().execute({ userId: 1 });
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
### Schema Builders
|
|
390
|
+
|
|
391
|
+
#### `Table(name)` / `TableBuilder`
|
|
392
|
+
|
|
393
|
+
Factory function and builder class for defining database tables.
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
import { Table } from "@simplysm/orm-common";
|
|
397
|
+
|
|
398
|
+
const User = Table("User")
|
|
399
|
+
.database("mydb")
|
|
400
|
+
.columns((c) => ({
|
|
401
|
+
id: c.bigint().autoIncrement(),
|
|
402
|
+
name: c.varchar(100),
|
|
403
|
+
email: c.varchar(200).nullable(),
|
|
404
|
+
status: c.varchar(20).default("active"),
|
|
405
|
+
createdAt: c.datetime(),
|
|
406
|
+
}))
|
|
407
|
+
.primaryKey("id")
|
|
408
|
+
.indexes((i) => [
|
|
409
|
+
i.index("email").unique(),
|
|
410
|
+
i.index("name", "createdAt").orderBy("ASC", "DESC"),
|
|
411
|
+
])
|
|
412
|
+
.relations((r) => ({
|
|
413
|
+
posts: r.foreignKeyTarget(() => Post, "author"),
|
|
414
|
+
profile: r.foreignKeyTarget(() => Profile, "user").single(),
|
|
415
|
+
}));
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`TableBuilder` fluent methods:
|
|
419
|
+
|
|
420
|
+
| Method | Description |
|
|
421
|
+
|--------|-------------|
|
|
422
|
+
| `.database(db)` | Set database name |
|
|
423
|
+
| `.schema(schema)` | Set schema name (MSSQL/PostgreSQL) |
|
|
424
|
+
| `.description(desc)` | Set table description (DDL comment) |
|
|
425
|
+
| `.columns(fn)` | Define columns via column factory |
|
|
426
|
+
| `.primaryKey(...columns)` | Set primary key (supports composite PK) |
|
|
427
|
+
| `.indexes(fn)` | Define indexes via index factory |
|
|
428
|
+
| `.relations(fn)` | Define relationships via relation factory |
|
|
429
|
+
|
|
430
|
+
`TableBuilder` type inference properties:
|
|
431
|
+
|
|
432
|
+
| Property | Description |
|
|
433
|
+
|----------|-------------|
|
|
434
|
+
| `$infer` | Full row type (columns + relations) |
|
|
435
|
+
| `$inferColumns` | Columns-only type |
|
|
436
|
+
| `$inferInsert` | INSERT type (required fields required, autoIncrement/nullable/default optional) |
|
|
437
|
+
| `$inferUpdate` | UPDATE type (all fields optional) |
|
|
438
|
+
| `$columns` | Raw `ColumnBuilderRecord` |
|
|
439
|
+
| `$relations` | Raw `RelationBuilderRecord` |
|
|
440
|
+
|
|
441
|
+
#### `View(name)` / `ViewBuilder`
|
|
442
|
+
|
|
443
|
+
Factory function and builder class for defining database views.
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
import { View } from "@simplysm/orm-common";
|
|
447
|
+
|
|
448
|
+
const ActiveUsers = View("ActiveUsers")
|
|
449
|
+
.database("mydb")
|
|
450
|
+
.query((db: MyDb) =>
|
|
451
|
+
db.user()
|
|
452
|
+
.where((u) => [expr.eq(u.status, "active")])
|
|
453
|
+
.select((u) => ({ id: u.id, name: u.name }))
|
|
454
|
+
);
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
`ViewBuilder` fluent methods:
|
|
458
|
+
|
|
459
|
+
| Method | Description |
|
|
460
|
+
|--------|-------------|
|
|
461
|
+
| `.database(db)` | Set database name |
|
|
462
|
+
| `.schema(schema)` | Set schema name |
|
|
463
|
+
| `.description(desc)` | Set view description |
|
|
464
|
+
| `.query(viewFn)` | Define the view SELECT query; takes a `DbContextBase` and returns `Queryable` |
|
|
465
|
+
| `.relations(fn)` | Define relationships (only `relationKey`/`relationKeyTarget`, not FK) |
|
|
466
|
+
|
|
467
|
+
`ViewBuilder` type inference properties:
|
|
468
|
+
|
|
469
|
+
| Property | Description |
|
|
470
|
+
|----------|-------------|
|
|
471
|
+
| `$infer` | View row type |
|
|
472
|
+
| `$relations` | Raw `RelationBuilderRecord` |
|
|
473
|
+
|
|
474
|
+
#### `Procedure(name)` / `ProcedureBuilder`
|
|
475
|
+
|
|
476
|
+
Factory function and builder class for defining stored procedures.
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
import { Procedure } from "@simplysm/orm-common";
|
|
480
|
+
|
|
481
|
+
const GetUserById = Procedure("GetUserById")
|
|
482
|
+
.database("mydb")
|
|
483
|
+
.params((c) => ({
|
|
484
|
+
userId: c.bigint(),
|
|
485
|
+
}))
|
|
486
|
+
.returns((c) => ({
|
|
487
|
+
id: c.bigint(),
|
|
488
|
+
name: c.varchar(100),
|
|
489
|
+
email: c.varchar(200).nullable(),
|
|
490
|
+
}))
|
|
491
|
+
.body("SELECT id, name, email FROM User WHERE id = userId");
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
`ProcedureBuilder` fluent methods:
|
|
495
|
+
|
|
496
|
+
| Method | Description |
|
|
497
|
+
|--------|-------------|
|
|
498
|
+
| `.database(db)` | Set database name |
|
|
499
|
+
| `.schema(schema)` | Set schema name |
|
|
500
|
+
| `.description(desc)` | Set procedure description |
|
|
501
|
+
| `.params(fn)` | Define input parameters |
|
|
502
|
+
| `.returns(fn)` | Define result columns |
|
|
503
|
+
| `.body(sql)` | Set procedure body SQL (DBMS-specific syntax) |
|
|
504
|
+
|
|
505
|
+
`ProcedureBuilder` type inference properties:
|
|
506
|
+
|
|
507
|
+
| Property | Description |
|
|
508
|
+
|----------|-------------|
|
|
509
|
+
| `$params` | Raw parameter `ColumnBuilderRecord` |
|
|
510
|
+
| `$returns` | Raw returns `ColumnBuilderRecord` |
|
|
511
|
+
|
|
512
|
+
### Column Types (`createColumnFactory`)
|
|
513
|
+
|
|
514
|
+
Used inside `.columns()`, `.params()`, and `.returns()`, and also directly in migrations.
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
import { createColumnFactory } from "@simplysm/orm-common";
|
|
518
|
+
|
|
519
|
+
const c = createColumnFactory();
|
|
520
|
+
// Used in migrations:
|
|
521
|
+
await db.addColumn({ database: "mydb", name: "User" }, "status", c.varchar(20).nullable());
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Available column type methods:
|
|
525
|
+
|
|
526
|
+
| Method | SQL Type | TypeScript Type |
|
|
527
|
+
|--------|----------|-----------------|
|
|
528
|
+
| `c.int()` | INT | `number` |
|
|
529
|
+
| `c.bigint()` | BIGINT | `number` |
|
|
530
|
+
| `c.float()` | FLOAT | `number` |
|
|
531
|
+
| `c.double()` | DOUBLE | `number` |
|
|
532
|
+
| `c.decimal(precision, scale?)` | DECIMAL | `number` |
|
|
533
|
+
| `c.varchar(length)` | VARCHAR | `string` |
|
|
534
|
+
| `c.char(length)` | CHAR | `string` |
|
|
535
|
+
| `c.text()` | TEXT | `string` |
|
|
536
|
+
| `c.binary()` | LONGBLOB/VARBINARY(MAX)/BYTEA | `Bytes` (Uint8Array) |
|
|
537
|
+
| `c.boolean()` | TINYINT(1)/BIT/BOOLEAN | `boolean` |
|
|
538
|
+
| `c.datetime()` | DATETIME | `DateTime` |
|
|
539
|
+
| `c.date()` | DATE | `DateOnly` |
|
|
540
|
+
| `c.time()` | TIME | `Time` |
|
|
541
|
+
| `c.uuid()` | BINARY(16)/UNIQUEIDENTIFIER/UUID | `Uuid` |
|
|
542
|
+
|
|
543
|
+
`ColumnBuilder` modifier methods:
|
|
544
|
+
|
|
545
|
+
| Method | Description |
|
|
546
|
+
|--------|-------------|
|
|
547
|
+
| `.autoIncrement()` | Auto-increment; excluded from INSERT required fields |
|
|
548
|
+
| `.nullable()` | Allow NULL; adds `undefined` to the TypeScript type |
|
|
549
|
+
| `.default(value)` | Default value; makes the field optional in INSERT |
|
|
550
|
+
| `.description(desc)` | Column description (DDL comment) |
|
|
551
|
+
|
|
552
|
+
### Relation Builders
|
|
553
|
+
|
|
554
|
+
Used inside `.relations()`:
|
|
555
|
+
|
|
556
|
+
#### `r.foreignKey(columns, targetFn)` → `ForeignKeyBuilder`
|
|
557
|
+
|
|
558
|
+
N:1 relationship with DB FK constraint.
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
const Post = Table("Post")
|
|
562
|
+
.columns((c) => ({ id: c.bigint().autoIncrement(), authorId: c.bigint() }))
|
|
563
|
+
.primaryKey("id")
|
|
564
|
+
.relations((r) => ({
|
|
565
|
+
author: r.foreignKey(["authorId"], () => User),
|
|
566
|
+
}));
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
#### `r.foreignKeyTarget(targetTableFn, relationName)` → `ForeignKeyTargetBuilder`
|
|
570
|
+
|
|
571
|
+
1:N reverse reference. Results in an array by default; call `.single()` for 1:1.
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
const User = Table("User")
|
|
575
|
+
.columns((c) => ({ id: c.bigint().autoIncrement() }))
|
|
576
|
+
.primaryKey("id")
|
|
577
|
+
.relations((r) => ({
|
|
578
|
+
posts: r.foreignKeyTarget(() => Post, "author"), // 1:N → Post[]
|
|
579
|
+
profile: r.foreignKeyTarget(() => Profile, "user").single(), // 1:1 → Profile
|
|
580
|
+
}));
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
#### `r.relationKey(columns, targetFn)` → `RelationKeyBuilder`
|
|
584
|
+
|
|
585
|
+
N:1 logical relationship without a DB FK constraint. Available for both tables and views.
|
|
586
|
+
|
|
587
|
+
#### `r.relationKeyTarget(targetTableFn, relationName)` → `RelationKeyTargetBuilder`
|
|
588
|
+
|
|
589
|
+
1:N logical reverse reference without a DB FK constraint. Call `.single()` for 1:1.
|
|
590
|
+
|
|
591
|
+
#### `IndexBuilder` / `createIndexFactory`
|
|
592
|
+
|
|
593
|
+
Index builder, used inside `.indexes()`.
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
Table("User")
|
|
597
|
+
.indexes((i) => [
|
|
598
|
+
i.index("email").unique(),
|
|
599
|
+
i.index("name").name("IX_User_Name"),
|
|
600
|
+
i.index("createdAt").orderBy("DESC"),
|
|
601
|
+
i.index("status", "createdAt").orderBy("ASC", "DESC"),
|
|
602
|
+
]);
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
`IndexBuilder` methods:
|
|
606
|
+
|
|
607
|
+
| Method | Description |
|
|
608
|
+
|--------|-------------|
|
|
609
|
+
| `.unique()` | Create a unique index |
|
|
610
|
+
| `.name(name)` | Set index name |
|
|
611
|
+
| `.orderBy(...dirs)` | Set sort direction per column (`"ASC"` or `"DESC"`) |
|
|
612
|
+
| `.description(desc)` | Index description |
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
### SQL Expressions (`expr`)
|
|
617
|
+
|
|
618
|
+
The `expr` object provides all SQL expression builders. All methods return `ExprUnit` (SELECT expressions) or `WhereExprUnit` (WHERE conditions).
|
|
619
|
+
|
|
620
|
+
```typescript
|
|
621
|
+
import { expr } from "@simplysm/orm-common";
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
#### Value / Column Creation
|
|
625
|
+
|
|
626
|
+
| Method | Description |
|
|
627
|
+
|--------|-------------|
|
|
628
|
+
| `expr.val(dataType, value)` | Wrap a literal value as `ExprUnit` |
|
|
629
|
+
| `expr.col(dataType, ...path)` | Create a column reference (internal use) |
|
|
630
|
+
| `expr.raw(dataType)\`SQL ${interpolation}\`` | Raw SQL with interpolated `ExprUnit` values |
|
|
631
|
+
|
|
632
|
+
```typescript
|
|
633
|
+
expr.val("string", "active")
|
|
634
|
+
expr.val("number", 100)
|
|
635
|
+
expr.val("DateOnly", DateOnly.today())
|
|
636
|
+
expr.raw("number")`ARRAY_LENGTH(${u.tags}, 1)`
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
#### Comparison Expressions (WHERE)
|
|
640
|
+
|
|
641
|
+
| Method | SQL | Description |
|
|
642
|
+
|--------|-----|-------------|
|
|
643
|
+
| `expr.eq(source, target)` | `<=>` / IS NULL | NULL-safe equality |
|
|
644
|
+
| `expr.gt(source, target)` | `>` | Greater than |
|
|
645
|
+
| `expr.lt(source, target)` | `<` | Less than |
|
|
646
|
+
| `expr.gte(source, target)` | `>=` | Greater than or equal |
|
|
647
|
+
| `expr.lte(source, target)` | `<=` | Less than or equal |
|
|
648
|
+
| `expr.between(source, from?, to?)` | `BETWEEN` | Range; `undefined` means unbounded |
|
|
649
|
+
| `expr.null(source)` | `IS NULL` | Null check |
|
|
650
|
+
| `expr.like(source, pattern)` | `LIKE` | Pattern matching (`%`, `_`) |
|
|
651
|
+
| `expr.regexp(source, pattern)` | `REGEXP` | Regular expression matching |
|
|
652
|
+
| `expr.in(source, values)` | `IN (...)` | Value list match |
|
|
653
|
+
| `expr.inQuery(source, query)` | `IN (SELECT ...)` | Subquery match (single-column SELECT) |
|
|
654
|
+
| `expr.exists(query)` | `EXISTS (...)` | Subquery existence check |
|
|
655
|
+
|
|
656
|
+
```typescript
|
|
657
|
+
db.user()
|
|
658
|
+
.where((u) => [
|
|
659
|
+
expr.eq(u.status, "active"),
|
|
660
|
+
expr.gte(u.age, 18),
|
|
661
|
+
expr.between(u.score, 0, 100),
|
|
662
|
+
expr.null(u.deletedAt),
|
|
663
|
+
expr.in(u.role, ["admin", "manager"]),
|
|
664
|
+
expr.like(u.name, "John%"),
|
|
665
|
+
])
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
#### Logical Expressions (WHERE)
|
|
669
|
+
|
|
670
|
+
| Method | SQL | Description |
|
|
671
|
+
|--------|-----|-------------|
|
|
672
|
+
| `expr.not(arg)` | `NOT (...)` | Negate a condition |
|
|
673
|
+
| `expr.and(conditions)` | `(... AND ...)` | All conditions must be true |
|
|
674
|
+
| `expr.or(conditions)` | `(... OR ...)` | At least one condition must be true |
|
|
675
|
+
|
|
676
|
+
#### String Expressions
|
|
677
|
+
|
|
678
|
+
| Method | SQL | Description |
|
|
679
|
+
|--------|-----|-------------|
|
|
680
|
+
| `expr.concat(...args)` | `CONCAT(...)` | Concatenate strings; NULL treated as empty string |
|
|
681
|
+
| `expr.left(source, length)` | `LEFT(...)` | Extract N characters from left |
|
|
682
|
+
| `expr.right(source, length)` | `RIGHT(...)` | Extract N characters from right |
|
|
683
|
+
| `expr.trim(source)` | `TRIM(...)` | Remove surrounding whitespace |
|
|
684
|
+
| `expr.padStart(source, length, fillString)` | `LPAD(...)` | Left-pad to target length |
|
|
685
|
+
| `expr.replace(source, from, to)` | `REPLACE(...)` | Replace all occurrences |
|
|
686
|
+
| `expr.upper(source)` | `UPPER(...)` | Uppercase |
|
|
687
|
+
| `expr.lower(source)` | `LOWER(...)` | Lowercase |
|
|
688
|
+
| `expr.length(source)` | `CHAR_LENGTH(...)` | Character length |
|
|
689
|
+
| `expr.byteLength(source)` | `OCTET_LENGTH(...)` | Byte length (UTF-8: Korean = 3 bytes) |
|
|
690
|
+
| `expr.substring(source, start, length?)` | `SUBSTRING(...)` | Extract substring (1-based index) |
|
|
691
|
+
| `expr.indexOf(source, search)` | `LOCATE(...)` | Find position (1-based; 0 if not found) |
|
|
692
|
+
|
|
693
|
+
#### Numeric Expressions
|
|
694
|
+
|
|
695
|
+
| Method | SQL | Description |
|
|
696
|
+
|--------|-----|-------------|
|
|
697
|
+
| `expr.abs(source)` | `ABS(...)` | Absolute value |
|
|
698
|
+
| `expr.round(source, digits)` | `ROUND(...)` | Round to N decimal places |
|
|
699
|
+
| `expr.ceil(source)` | `CEILING(...)` | Round up |
|
|
700
|
+
| `expr.floor(source)` | `FLOOR(...)` | Round down |
|
|
701
|
+
|
|
702
|
+
#### Date Expressions
|
|
703
|
+
|
|
704
|
+
| Method | SQL | Description |
|
|
705
|
+
|--------|-----|-------------|
|
|
706
|
+
| `expr.year(source)` | `YEAR(...)` | Extract year (4-digit) |
|
|
707
|
+
| `expr.month(source)` | `MONTH(...)` | Extract month (1–12) |
|
|
708
|
+
| `expr.day(source)` | `DAY(...)` | Extract day (1–31) |
|
|
709
|
+
| `expr.hour(source)` | `HOUR(...)` | Extract hour (0–23) |
|
|
710
|
+
| `expr.minute(source)` | `MINUTE(...)` | Extract minute (0–59) |
|
|
711
|
+
| `expr.second(source)` | `SECOND(...)` | Extract second (0–59) |
|
|
712
|
+
| `expr.isoWeek(source)` | `WEEK(..., 3)` | ISO 8601 week number (1–53, Mon start) |
|
|
713
|
+
| `expr.isoWeekStartDate(source)` | — | Monday of the week containing the date |
|
|
714
|
+
| `expr.isoYearMonth(source)` | — | First day of the month (YYYY-MM-01) |
|
|
715
|
+
| `expr.dateDiff(separator, from, to)` | `DATEDIFF(...)` | Date difference in specified unit |
|
|
716
|
+
| `expr.dateAdd(separator, source, value)` | `DATEADD(...)` | Add an interval to a date |
|
|
717
|
+
| `expr.formatDate(source, format)` | `DATE_FORMAT(...)` | Format date as string (DBMS-specific format strings) |
|
|
718
|
+
|
|
719
|
+
`DateSeparator` values: `"year"`, `"month"`, `"day"`, `"hour"`, `"minute"`, `"second"`
|
|
720
|
+
|
|
721
|
+
#### Conditional Expressions
|
|
722
|
+
|
|
723
|
+
| Method | SQL | Description |
|
|
724
|
+
|--------|-----|-------------|
|
|
725
|
+
| `expr.ifNull(...args)` | `COALESCE(...)` | Return first non-null value |
|
|
726
|
+
| `expr.nullIf(source, value)` | `NULLIF(...)` | Return NULL if source equals value |
|
|
727
|
+
| `expr.is(condition)` | — | Convert WHERE expression to boolean `ExprUnit` |
|
|
728
|
+
| `expr.if(condition, then, else_)` | `IF(...)` / `CASE` | Ternary conditional |
|
|
729
|
+
| `expr.switch<T>().case(...).default(...)` | `CASE WHEN ... END` | Multi-branch conditional |
|
|
730
|
+
|
|
731
|
+
```typescript
|
|
732
|
+
// COALESCE
|
|
733
|
+
expr.ifNull(u.nickname, u.name, "Guest")
|
|
734
|
+
|
|
735
|
+
// Ternary
|
|
736
|
+
expr.if(expr.gte(u.age, 18), "adult", "minor")
|
|
737
|
+
|
|
738
|
+
// CASE WHEN
|
|
739
|
+
expr.switch<string>()
|
|
740
|
+
.case(expr.gte(u.score, 90), "A")
|
|
741
|
+
.case(expr.gte(u.score, 80), "B")
|
|
742
|
+
.default("F")
|
|
743
|
+
|
|
744
|
+
// Boolean from condition
|
|
745
|
+
db.user().select((u) => ({
|
|
746
|
+
isActive: expr.is(expr.eq(u.status, "active")),
|
|
747
|
+
}))
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
#### Aggregate Expressions
|
|
751
|
+
|
|
752
|
+
| Method | SQL | Description |
|
|
753
|
+
|--------|-----|-------------|
|
|
754
|
+
| `expr.count(arg?, distinct?)` | `COUNT(...)` | Row count; omit arg for `COUNT(*)` |
|
|
755
|
+
| `expr.sum(arg)` | `SUM(...)` | Sum; NULL if all values are NULL |
|
|
756
|
+
| `expr.avg(arg)` | `AVG(...)` | Average; NULL if all values are NULL |
|
|
757
|
+
| `expr.max(arg)` | `MAX(...)` | Maximum; NULL if all values are NULL |
|
|
758
|
+
| `expr.min(arg)` | `MIN(...)` | Minimum; NULL if all values are NULL |
|
|
759
|
+
| `expr.greatest(...args)` | `GREATEST(...)` | Largest among multiple values |
|
|
760
|
+
| `expr.least(...args)` | `LEAST(...)` | Smallest among multiple values |
|
|
761
|
+
|
|
762
|
+
#### Window Functions
|
|
763
|
+
|
|
764
|
+
All window functions accept a `WinSpecInput`:
|
|
765
|
+
|
|
766
|
+
```typescript
|
|
767
|
+
interface WinSpecInput {
|
|
768
|
+
partitionBy?: ExprInput<ColumnPrimitive>[];
|
|
769
|
+
orderBy?: [ExprInput<ColumnPrimitive>, ("ASC" | "DESC")?][];
|
|
222
770
|
}
|
|
223
771
|
```
|
|
224
772
|
|
|
225
|
-
|
|
773
|
+
| Method | SQL | Description |
|
|
774
|
+
|--------|-----|-------------|
|
|
775
|
+
| `expr.rowNumber(spec)` | `ROW_NUMBER() OVER (...)` | Sequential row number within partition |
|
|
776
|
+
| `expr.rank(spec)` | `RANK() OVER (...)` | Rank; ties get same rank, next skipped |
|
|
777
|
+
| `expr.denseRank(spec)` | `DENSE_RANK() OVER (...)` | Dense rank; ties get same rank, no skipping |
|
|
778
|
+
| `expr.ntile(n, spec)` | `NTILE(n) OVER (...)` | Split partition into N buckets |
|
|
779
|
+
| `expr.lag(column, spec, options?)` | `LAG(...) OVER (...)` | Previous row value |
|
|
780
|
+
| `expr.lead(column, spec, options?)` | `LEAD(...) OVER (...)` | Next row value |
|
|
781
|
+
| `expr.firstValue(column, spec)` | `FIRST_VALUE(...) OVER (...)` | First value in window |
|
|
782
|
+
| `expr.lastValue(column, spec)` | `LAST_VALUE(...) OVER (...)` | Last value in window |
|
|
783
|
+
| `expr.sumOver(column, spec)` | `SUM(...) OVER (...)` | Cumulative/windowed sum |
|
|
784
|
+
| `expr.avgOver(column, spec)` | `AVG(...) OVER (...)` | Windowed average |
|
|
785
|
+
| `expr.countOver(spec, column?)` | `COUNT(*) OVER (...)` | Windowed row count |
|
|
786
|
+
| `expr.minOver(column, spec)` | `MIN(...) OVER (...)` | Windowed minimum |
|
|
787
|
+
| `expr.maxOver(column, spec)` | `MAX(...) OVER (...)` | Windowed maximum |
|
|
788
|
+
|
|
789
|
+
```typescript
|
|
790
|
+
db.order().select((o) => ({
|
|
791
|
+
...o,
|
|
792
|
+
rowNum: expr.rowNumber({
|
|
793
|
+
partitionBy: [o.userId],
|
|
794
|
+
orderBy: [[o.createdAt, "DESC"]],
|
|
795
|
+
}),
|
|
796
|
+
runningTotal: expr.sumOver(o.amount, {
|
|
797
|
+
partitionBy: [o.userId],
|
|
798
|
+
orderBy: [[o.createdAt, "ASC"]],
|
|
799
|
+
}),
|
|
800
|
+
}))
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
#### Other Expressions
|
|
804
|
+
|
|
805
|
+
| Method | SQL | Description |
|
|
806
|
+
|--------|-----|-------------|
|
|
807
|
+
| `expr.cast(source, targetType)` | `CAST(... AS ...)` | Type conversion |
|
|
808
|
+
| `expr.subquery(dataType, queryable)` | `(SELECT ...)` | Scalar subquery in SELECT |
|
|
809
|
+
| `expr.random()` | `RAND()` / `RANDOM()` | Random number 0–1 |
|
|
810
|
+
| `expr.rowNum()` | — | Row number without window spec |
|
|
811
|
+
| `expr.toExpr(value)` | — | Convert `ExprInput` to raw `Expr` AST |
|
|
812
|
+
|
|
813
|
+
```typescript
|
|
814
|
+
// CAST
|
|
815
|
+
expr.cast(o.id, { type: "varchar", length: 20 })
|
|
816
|
+
|
|
817
|
+
// Scalar subquery
|
|
818
|
+
expr.subquery(
|
|
819
|
+
"number",
|
|
820
|
+
db.post().where((p) => [expr.eq(p.userId, u.id)]).select(() => ({ cnt: expr.count() }))
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
// Random sort
|
|
824
|
+
db.user().orderBy(() => expr.random()).limit(0, 10)
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
---
|
|
828
|
+
|
|
829
|
+
### Models
|
|
830
|
+
|
|
831
|
+
#### `_Migration` (internal)
|
|
832
|
+
|
|
833
|
+
The internal system table used by `initialize()` to track applied migrations. It is automatically added to every `DbContextDef` by `defineDbContext` as `_migration`.
|
|
834
|
+
|
|
835
|
+
```typescript
|
|
836
|
+
// Accessible via the db instance (read-only, internal use)
|
|
837
|
+
const applied = await db._migration().result();
|
|
838
|
+
// Returns: { code: string }[]
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
---
|
|
842
|
+
|
|
843
|
+
### Query Builder (SQL Generation)
|
|
844
|
+
|
|
845
|
+
#### `createQueryBuilder(dialect)`
|
|
846
|
+
|
|
847
|
+
Creates a dialect-specific `QueryBuilderBase` for converting `QueryDef` JSON AST to SQL strings.
|
|
848
|
+
|
|
849
|
+
```typescript
|
|
850
|
+
import { createQueryBuilder } from "@simplysm/orm-common";
|
|
851
|
+
|
|
852
|
+
const builder = createQueryBuilder("mysql"); // "mysql" | "mssql" | "postgresql"
|
|
853
|
+
const { sql, resultSetIndex, resultSetStride } = builder.build(queryDef);
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
The `build(def)` method accepts any `QueryDef` and returns `QueryBuildResult`:
|
|
857
|
+
|
|
858
|
+
| Property | Type | Description |
|
|
859
|
+
|----------|------|-------------|
|
|
860
|
+
| `sql` | `string` | Generated SQL string |
|
|
861
|
+
| `resultSetIndex` | `number?` | Which result set to extract (0-based) |
|
|
862
|
+
| `resultSetStride` | `number?` | For multi-INSERT: extract every N-th result set |
|
|
863
|
+
|
|
864
|
+
Available builders (exposed for extension):
|
|
865
|
+
|
|
866
|
+
| Class | Description |
|
|
867
|
+
|-------|-------------|
|
|
868
|
+
| `QueryBuilderBase` | Abstract base class |
|
|
869
|
+
| `ExprRendererBase` | Abstract expression renderer base |
|
|
870
|
+
| `MysqlQueryBuilder` | MySQL implementation |
|
|
871
|
+
| `MysqlExprRenderer` | MySQL expression renderer |
|
|
872
|
+
| `MssqlQueryBuilder` | MSSQL implementation |
|
|
873
|
+
| `MssqlExprRenderer` | MSSQL expression renderer |
|
|
874
|
+
| `PostgresqlQueryBuilder` | PostgreSQL implementation |
|
|
875
|
+
| `PostgresqlExprRenderer` | PostgreSQL expression renderer |
|
|
876
|
+
|
|
877
|
+
---
|
|
226
878
|
|
|
227
|
-
###
|
|
879
|
+
### Low-Level Utilities
|
|
228
880
|
|
|
229
|
-
|
|
881
|
+
#### `queryable(db, tableOrView, as?)`
|
|
882
|
+
|
|
883
|
+
Factory that returns a `() => Queryable` accessor. Used internally by `createDbContext`. Available for building custom db context wrappers.
|
|
230
884
|
|
|
231
885
|
```typescript
|
|
232
886
|
import { queryable } from "@simplysm/orm-common";
|
|
@@ -235,9 +889,9 @@ const getUserQueryable = queryable(db, User);
|
|
|
235
889
|
const users = await getUserQueryable().where((u) => [expr.eq(u.isActive, true)]).result();
|
|
236
890
|
```
|
|
237
891
|
|
|
238
|
-
|
|
892
|
+
#### `executable(db, procedureBuilder)`
|
|
239
893
|
|
|
240
|
-
Factory
|
|
894
|
+
Factory that returns a `() => Executable` accessor. Used internally by `createDbContext`.
|
|
241
895
|
|
|
242
896
|
```typescript
|
|
243
897
|
import { executable } from "@simplysm/orm-common";
|
|
@@ -246,9 +900,9 @@ const getUser = executable(db, GetUserById);
|
|
|
246
900
|
const result = await getUser().execute({ userId: 1 });
|
|
247
901
|
```
|
|
248
902
|
|
|
249
|
-
|
|
903
|
+
#### `parseQueryResult(rawResults, meta)`
|
|
250
904
|
|
|
251
|
-
Parses raw DB query results into typed TypeScript objects. Handles type coercion and JOIN result grouping/nesting.
|
|
905
|
+
Parses raw DB query results into typed TypeScript objects. Handles type coercion (e.g., `"1"` → `number`) and JOIN result grouping/nesting. Returns `undefined` for empty or all-null results.
|
|
252
906
|
|
|
253
907
|
```typescript
|
|
254
908
|
import { parseQueryResult } from "@simplysm/orm-common";
|
|
@@ -258,19 +912,36 @@ const meta = {
|
|
|
258
912
|
joins: {},
|
|
259
913
|
};
|
|
260
914
|
const result = await parseQueryResult(rawResults, meta);
|
|
915
|
+
// Returns TRecord[] | undefined
|
|
261
916
|
```
|
|
262
917
|
|
|
263
|
-
|
|
918
|
+
#### `parseSearchQuery(searchText)`
|
|
264
919
|
|
|
265
920
|
Parses a search query string into SQL LIKE patterns for use with `.search()`.
|
|
266
921
|
|
|
267
922
|
```typescript
|
|
268
923
|
import { parseSearchQuery } from "@simplysm/orm-common";
|
|
269
924
|
|
|
270
|
-
const parsed = parseSearchQuery('apple "exact phrase" +required -excluded');
|
|
271
|
-
// {
|
|
925
|
+
const parsed = parseSearchQuery('apple "exact phrase" +required -excluded app*');
|
|
926
|
+
// {
|
|
927
|
+
// or: ["%apple%", "app%"],
|
|
928
|
+
// must: ["%exact phrase%", "%required%"],
|
|
929
|
+
// not: ["%excluded%"]
|
|
930
|
+
// }
|
|
272
931
|
```
|
|
273
932
|
|
|
933
|
+
Search syntax:
|
|
934
|
+
|
|
935
|
+
| Syntax | Meaning |
|
|
936
|
+
|--------|---------|
|
|
937
|
+
| `term` | OR: contains term |
|
|
938
|
+
| `+term` | MUST: must contain term (AND) |
|
|
939
|
+
| `-term` | NOT: must not contain term |
|
|
940
|
+
| `"exact phrase"` | MUST: exact phrase match |
|
|
941
|
+
| `term*` | Starts with (wildcard) |
|
|
942
|
+
| `*term` | Ends with |
|
|
943
|
+
| `\*`, `\+`, `\-`, `\%`, `\"`, `\\` | Escape special characters |
|
|
944
|
+
|
|
274
945
|
Returns `ParsedSearchQuery`:
|
|
275
946
|
|
|
276
947
|
| Property | Type | Description |
|
|
@@ -279,33 +950,24 @@ Returns `ParsedSearchQuery`:
|
|
|
279
950
|
| `must` | `string[]` | Required AND conditions (LIKE patterns) |
|
|
280
951
|
| `not` | `string[]` | NOT conditions (LIKE patterns) |
|
|
281
952
|
|
|
282
|
-
|
|
953
|
+
#### `getMatchedPrimaryKeys(fkCols, targetTable)`
|
|
283
954
|
|
|
284
|
-
|
|
955
|
+
Matches FK column array against the target table's primary key and returns the PK column name array. Used internally by `include()` and custom JOIN logic.
|
|
285
956
|
|
|
286
957
|
```typescript
|
|
287
|
-
import {
|
|
958
|
+
import { getMatchedPrimaryKeys } from "@simplysm/orm-common";
|
|
288
959
|
|
|
289
|
-
const
|
|
290
|
-
|
|
960
|
+
const pkCols = getMatchedPrimaryKeys(["userId"], User);
|
|
961
|
+
// Returns: ["id"]
|
|
291
962
|
```
|
|
292
963
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
Creates a column type factory used in `TableBuilder.columns()` and `ProcedureBuilder.params()/returns()`. Can also be used standalone for DDL migrations.
|
|
296
|
-
|
|
297
|
-
```typescript
|
|
298
|
-
import { createColumnFactory } from "@simplysm/orm-common";
|
|
299
|
-
|
|
300
|
-
const c = createColumnFactory();
|
|
301
|
-
await db.addColumn({ database: "mydb", name: "User" }, "status", c.varchar(20).nullable());
|
|
302
|
-
```
|
|
964
|
+
---
|
|
303
965
|
|
|
304
966
|
## Expression Types
|
|
305
967
|
|
|
306
968
|
### `ExprUnit<TPrimitive>`
|
|
307
969
|
|
|
308
|
-
Type-safe expression wrapper. All `expr.*` methods return `ExprUnit`.
|
|
970
|
+
Type-safe expression wrapper. All `expr.*` SELECT/value methods return `ExprUnit`.
|
|
309
971
|
|
|
310
972
|
```typescript
|
|
311
973
|
import { ExprUnit } from "@simplysm/orm-common";
|
|
@@ -321,6 +983,10 @@ import { ExprUnit } from "@simplysm/orm-common";
|
|
|
321
983
|
|
|
322
984
|
Wrapper for WHERE condition expressions. All comparison and logical `expr.*` methods return `WhereExprUnit`.
|
|
323
985
|
|
|
986
|
+
```typescript
|
|
987
|
+
import { WhereExprUnit } from "@simplysm/orm-common";
|
|
988
|
+
```
|
|
989
|
+
|
|
324
990
|
### `ExprInput<TPrimitive>`
|
|
325
991
|
|
|
326
992
|
Union type that accepts either an `ExprUnit<TPrimitive>` or a plain literal value. Most `expr.*` parameters accept `ExprInput` so you can pass raw values without wrapping in `expr.val()`.
|
|
@@ -329,22 +995,51 @@ Union type that accepts either an `ExprUnit<TPrimitive>` or a plain literal valu
|
|
|
329
995
|
type ExprInput<TPrimitive> = ExprUnit<TPrimitive> | TPrimitive;
|
|
330
996
|
```
|
|
331
997
|
|
|
332
|
-
### `QueryableRecord<TData>`
|
|
333
|
-
|
|
334
|
-
Maps a data record type to its expression counterpart. Each primitive field becomes `ExprUnit`, each array field becomes `QueryableRecord[]`, and each nested object becomes `QueryableRecord`.
|
|
335
|
-
|
|
336
998
|
### `SwitchExprBuilder<TPrimitive>`
|
|
337
999
|
|
|
338
|
-
Builder
|
|
1000
|
+
Builder returned by `expr.switch()`:
|
|
339
1001
|
|
|
340
1002
|
| Method | Description |
|
|
341
1003
|
|--------|-------------|
|
|
342
1004
|
| `.case(condition, then)` | Add WHEN ... THEN branch |
|
|
343
1005
|
| `.default(value)` | Add ELSE and finalize to `ExprUnit` |
|
|
344
1006
|
|
|
1007
|
+
### `QueryableRecord<TData>`
|
|
1008
|
+
|
|
1009
|
+
Maps a data record type to its expression counterpart. Each primitive field becomes `ExprUnit`, array relations become `QueryableRecord[]`, and nested objects become `QueryableRecord`.
|
|
1010
|
+
|
|
1011
|
+
### `NullableQueryableRecord<TData>`
|
|
1012
|
+
|
|
1013
|
+
Like `QueryableRecord` but all primitive fields have `| undefined` added (representing LEFT JOIN NULL propagation).
|
|
1014
|
+
|
|
1015
|
+
### `QueryableWriteRecord<TData>`
|
|
1016
|
+
|
|
1017
|
+
Write-side record type for `update()`/`upsert()` callbacks. Each field accepts `ExprInput<T>` (either `ExprUnit<T>` or a plain value).
|
|
1018
|
+
|
|
1019
|
+
### `UnwrapQueryableRecord<R>`
|
|
1020
|
+
|
|
1021
|
+
Reverse mapping from a `QueryableRecord` shape back to a plain `DataRecord`. Used to infer the result type of `select()`.
|
|
1022
|
+
|
|
1023
|
+
### `PathProxy<TObject>`
|
|
1024
|
+
|
|
1025
|
+
Type-safe path proxy used by `.include()`. Only non-primitive (relation) fields are accessible.
|
|
1026
|
+
|
|
1027
|
+
```typescript
|
|
1028
|
+
// TypeScript will error if you try to access a primitive column
|
|
1029
|
+
db.post().include((p) => p.author) // OK
|
|
1030
|
+
db.post().include((p) => p.author.company) // OK (chained)
|
|
1031
|
+
// db.post().include((p) => p.title) // Error: title is ColumnPrimitive
|
|
1032
|
+
```
|
|
1033
|
+
|
|
345
1034
|
### `toExpr(value)`
|
|
346
1035
|
|
|
347
|
-
Converts an `ExprInput` to a raw `Expr` JSON AST.
|
|
1036
|
+
Converts an `ExprInput` to a raw `Expr` JSON AST. Exposed for custom `QueryBuilder` extensions.
|
|
1037
|
+
|
|
1038
|
+
```typescript
|
|
1039
|
+
import { toExpr } from "@simplysm/orm-common";
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
---
|
|
348
1043
|
|
|
349
1044
|
## Type Reference
|
|
350
1045
|
|
|
@@ -398,28 +1093,15 @@ Converts an `ExprInput` to a raw `Expr` JSON AST. Used internally; exposed for c
|
|
|
398
1093
|
| `InferUpdateColumns<T>` | UPDATE type (all optional) |
|
|
399
1094
|
| `InferColumnExprs<T>` | Expression input type from a `ColumnBuilderRecord` |
|
|
400
1095
|
| `InferDeepRelations<T>` | Infer relation types (all optional) |
|
|
401
|
-
| `QueryableWriteRecord<TData>` | Write-side record type for update/upsert callbacks (accepts `ExprInput<T>`
|
|
1096
|
+
| `QueryableWriteRecord<TData>` | Write-side record type for update/upsert callbacks (accepts `ExprInput<T>`) |
|
|
402
1097
|
| `PathProxy<TObject>` | Type-safe path proxy for `.include()` navigation |
|
|
1098
|
+
| `DataToColumnBuilderRecord<TData>` | Convert `DataRecord` to `ColumnBuilderRecord` (for `insertInto` type checking) |
|
|
1099
|
+
| `RequiredInsertKeys<T>` | Keys that are required in INSERT (no autoIncrement, nullable, or default) |
|
|
1100
|
+
| `OptionalInsertKeys<T>` | Keys that are optional in INSERT |
|
|
1101
|
+
| `ExtractRelationTarget<T>` | Extract the TypeScript type of an FK/RelationKey target |
|
|
1102
|
+
| `ExtractRelationTargetResult<T>` | Extract the TypeScript type of an FKTarget/RelationKeyTarget (array or single) |
|
|
403
1103
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
orm-common uses **enhanced string escaping** instead of parameter binding due to its dynamic query nature.
|
|
407
|
-
Always perform input validation at the application level.
|
|
408
|
-
|
|
409
|
-
```typescript
|
|
410
|
-
// Bad: Direct user input
|
|
411
|
-
const userInput = req.query.name; // e.g. malicious SQL payload
|
|
412
|
-
await db.user().where((u) => [expr.eq(u.name, userInput)]).result();
|
|
413
|
-
|
|
414
|
-
// Good: Validate before use
|
|
415
|
-
const userName = validateUserName(req.query.name);
|
|
416
|
-
await db.user().where((u) => [expr.eq(u.name, userName)]).result();
|
|
417
|
-
|
|
418
|
-
// Better: Type coercion
|
|
419
|
-
const userId = Number(req.query.id);
|
|
420
|
-
if (Number.isNaN(userId)) throw new Error("Invalid ID");
|
|
421
|
-
await db.user().where((u) => [expr.eq(u.id, userId)]).result();
|
|
422
|
-
```
|
|
1104
|
+
---
|
|
423
1105
|
|
|
424
1106
|
## Quick Start
|
|
425
1107
|
|
|
@@ -464,6 +1146,26 @@ await db.connect(async () => {
|
|
|
464
1146
|
});
|
|
465
1147
|
```
|
|
466
1148
|
|
|
1149
|
+
## Security Notes
|
|
1150
|
+
|
|
1151
|
+
orm-common uses **enhanced string escaping** instead of parameter binding due to its dynamic query nature.
|
|
1152
|
+
Always perform input validation at the application level.
|
|
1153
|
+
|
|
1154
|
+
```typescript
|
|
1155
|
+
// Bad: Direct user input
|
|
1156
|
+
const userInput = req.query.name; // e.g. malicious SQL payload
|
|
1157
|
+
await db.user().where((u) => [expr.eq(u.name, userInput)]).result();
|
|
1158
|
+
|
|
1159
|
+
// Good: Validate before use
|
|
1160
|
+
const userName = validateUserName(req.query.name);
|
|
1161
|
+
await db.user().where((u) => [expr.eq(u.name, userName)]).result();
|
|
1162
|
+
|
|
1163
|
+
// Better: Type coercion
|
|
1164
|
+
const userId = Number(req.query.id);
|
|
1165
|
+
if (Number.isNaN(userId)) throw new Error("Invalid ID");
|
|
1166
|
+
await db.user().where((u) => [expr.eq(u.id, userId)]).result();
|
|
1167
|
+
```
|
|
1168
|
+
|
|
467
1169
|
## License
|
|
468
1170
|
|
|
469
1171
|
Apache-2.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysm/orm-common",
|
|
3
|
-
"version": "13.0.
|
|
3
|
+
"version": "13.0.43",
|
|
4
4
|
"description": "심플리즘 패키지 - ORM 모듈 (common)",
|
|
5
5
|
"author": "김석래",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -19,6 +19,6 @@
|
|
|
19
19
|
],
|
|
20
20
|
"sideEffects": false,
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@simplysm/core-common": "13.0.
|
|
22
|
+
"@simplysm/core-common": "13.0.43"
|
|
23
23
|
}
|
|
24
24
|
}
|