litedbmodel 0.22.3 → 1.0.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.
@@ -0,0 +1,259 @@
1
+ # `sql` Tagged Template Literal
2
+
3
+ The `sql` tagged template provides type-safe, parameterized SQL fragments for conditions, `withQuery`, `QUERY`, and `execute`.
4
+
5
+ ```typescript
6
+ import { sql } from 'litedbmodel';
7
+ ```
8
+
9
+ ---
10
+
11
+ ## Conditions
12
+
13
+ ### Column Tuples with `sql` Tag (Pattern A)
14
+
15
+ Use `[sql\`...\`, value]` tuples for operator conditions. The `sql` tag extracts the Column reference and preserves its value type, so the tuple's second element is type-checked:
16
+
17
+ ```typescript
18
+ await User.find([
19
+ [sql`${User.age} > ?`, 18], // ✅ number
20
+ [sql`${User.age} BETWEEN ? AND ?`, [18, 65]], // ✅ number[]
21
+ [sql`${User.name} LIKE ?`, '%test%'], // ✅ string
22
+ [sql`${User.status} IN (?)`, ['active', 'pending']], // ✅ string[]
23
+ sql`${User.deleted_at} IS NULL`, // ✅ no value needed
24
+ ]);
25
+ ```
26
+
27
+ Type errors are caught at compile time:
28
+
29
+ ```typescript
30
+ [sql`${User.age} > ?`, 'hello'] // ❌ Type error: string is not assignable to number
31
+ [sql`${User.name} LIKE ?`, 42] // ❌ Type error: number is not assignable to string
32
+ ```
33
+
34
+ ### Embedded Values (Pattern B)
35
+
36
+ Values can be interpolated directly in the template. The `sql` tag automatically converts them to `?` placeholders and extracts them as parameters:
37
+
38
+ ```typescript
39
+ await User.find([
40
+ sql`${User.age} > ${18}`,
41
+ sql`${User.age} BETWEEN ${18} AND ${65}`,
42
+ sql`${User.name} LIKE ${'%test%'}`,
43
+ sql`${User.status} IN ${['active', 'pending']}`,
44
+ sql`${User.deleted_at} IS NULL`,
45
+ ]);
46
+ ```
47
+
48
+ Both patterns produce the same internal representation and can be mixed freely.
49
+
50
+ ### SKIP Pattern
51
+
52
+ Both patterns work with `SKIP` for conditional conditions:
53
+
54
+ ```typescript
55
+ import { SKIP } from 'litedbmodel';
56
+
57
+ await User.find([
58
+ [User.deleted, false],
59
+ query.name ? [sql`${User.name} LIKE ?`, `%${query.name}%`] : SKIP,
60
+ [User.status, query.status ?? SKIP],
61
+ ]);
62
+ ```
63
+
64
+ ### OR Conditions
65
+
66
+ ```typescript
67
+ await User.find([
68
+ [sql`${User.age} >= ?`, 18],
69
+ User.or(
70
+ [[sql`${User.role} = ?`, 'admin']],
71
+ [[sql`${User.role} = ?`, 'moderator']],
72
+ ),
73
+ ]);
74
+ ```
75
+
76
+ ### Conditions Builder
77
+
78
+ The `Conditions` class provides an `addSql()` method for dynamic condition building:
79
+
80
+ ```typescript
81
+ const where = new Conditions<User>();
82
+ where.add(User.is_active, true);
83
+ where.addSql(sql`${User.age} > ?`, 18);
84
+ where.addSql(sql`${User.deleted_at} IS NULL`); // value-free
85
+ const users = await User.find(where.build());
86
+ ```
87
+
88
+ ---
89
+
90
+ ## `withQuery` — Parameterized Query-Based Models
91
+
92
+ The `sql` tag eliminates manual `$1`/`$2` placeholder numbering and DB dialect differences. Parameters are co-located with their SQL usage:
93
+
94
+ ```typescript
95
+ @model('sales_report')
96
+ class SalesReportModel extends DBModel {
97
+ @column() product_id?: number;
98
+ @column() total_revenue?: number;
99
+
100
+ static forPeriod(startDate: string, endDate: string) {
101
+ return this.withQuery(sql`
102
+ SELECT p.id AS product_id, SUM(oi.price) AS total_revenue
103
+ FROM products p
104
+ INNER JOIN order_items oi ON p.id = oi.product_id
105
+ INNER JOIN orders o ON oi.order_id = o.id
106
+ WHERE o.status = 'completed'
107
+ AND o.created_at >= ${startDate}
108
+ AND o.created_at < ${endDate}
109
+ GROUP BY p.id
110
+ `);
111
+ }
112
+ }
113
+
114
+ const Q1Report = SalesReport.forPeriod('2024-01-01', '2024-04-01');
115
+ const results = await Q1Report.find([
116
+ [sql`${SalesReport.total_revenue} > ?`, 10000],
117
+ ]);
118
+ ```
119
+
120
+ The `sql` tag always uses `?` placeholders internally. The dialect conversion (`?` → `$1`/`$2` for PostgreSQL) is handled transparently at execution time.
121
+
122
+ The `{ sql, params }` object form is still supported for backward compatibility.
123
+
124
+ ---
125
+
126
+ ## `QUERY` — Static Query-Based Models
127
+
128
+ Use the `sql` tag for `QUERY` to get Column and table name references that are refactoring-safe:
129
+
130
+ ```typescript
131
+ @model('user_activity')
132
+ class UserActivityModel extends DBModel {
133
+ @column() user_id?: number;
134
+ @column() user_name?: string;
135
+ @column() total_posts?: number;
136
+
137
+ static QUERY = sql`
138
+ SELECT
139
+ ${User.id} AS user_id,
140
+ ${User.name} AS user_name,
141
+ COUNT(${Post.id}) AS total_posts
142
+ FROM ${User.TABLE_NAME}
143
+ LEFT JOIN ${Post.TABLE_NAME} ON ${User.id} = ${Post.user_id}
144
+ GROUP BY ${User.id}, ${User.name}
145
+ `;
146
+ }
147
+ ```
148
+
149
+ When only Column references and `TABLE_NAME` objects are interpolated (no runtime values), the `sql` tag embeds them directly in the SQL string with no parameters.
150
+
151
+ String `QUERY` is still supported for backward compatibility.
152
+
153
+ ---
154
+
155
+ ## `execute` — Raw SQL Execution
156
+
157
+ `DBModel.execute()` accepts `sql` tagged templates:
158
+
159
+ ```typescript
160
+ await DBModel.execute(sql`SELECT process_daily_aggregates(${targetDate})`);
161
+ ```
162
+
163
+ String + params form is still supported:
164
+
165
+ ```typescript
166
+ await DBModel.execute('SELECT process_daily_aggregates($1)', [targetDate]);
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Interpolation Rules
172
+
173
+ The `sql` tag processes interpolated values according to their type:
174
+
175
+ | Interpolated Value | Processing | Example Output |
176
+ |---|---|---|
177
+ | `Column` | Embedded as column name | `age`, `name` |
178
+ | `{ TABLE_NAME }` | Embedded as table name | `users`, `posts` |
179
+ | `sql.raw(str)` | Embedded as raw SQL (no parameterization) | `ASC`, `DISTINCT` |
180
+ | `sql.ref(col)` | Embedded as `table.column` | `users.id` |
181
+ | `parentRef(col)` | Embedded as `table.column` | `users.id` |
182
+ | Nested `sql` fragment | SQL expanded, params merged | See below |
183
+ | Array | Expanded to `?, ?, ?` + params extracted | `?, ?, ?` |
184
+ | Primitive / Date | Replaced with `?` + param extracted | `?` |
185
+ | `null` / `undefined` | Embedded as `NULL` literal | `NULL` |
186
+
187
+ ### `sql.raw()` — Raw SQL Embedding
188
+
189
+ Use for dynamic SQL keywords that should not be parameterized:
190
+
191
+ ```typescript
192
+ const direction = sql.raw(isAsc ? 'ASC' : 'DESC');
193
+ const results = await Ranking.query(
194
+ sql`SELECT * FROM rankings ORDER BY score ${direction} LIMIT ${limit}`
195
+ );
196
+ ```
197
+
198
+ ### `sql.ref()` — Table-Qualified Column References
199
+
200
+ Use for JOINs where table disambiguation is needed:
201
+
202
+ ```typescript
203
+ sql`SELECT ${sql.ref(User.id)}, ${sql.ref(Post.title)}
204
+ FROM users
205
+ JOIN posts ON ${sql.ref(User.id)} = ${sql.ref(Post.user_id)}`
206
+ ```
207
+
208
+ | Interpolation | Output |
209
+ |---|---|
210
+ | `${User.id}` | `id` |
211
+ | `${sql.ref(User.id)}` | `users.id` |
212
+
213
+ ### Nested `sql` Fragments
214
+
215
+ A `sql` fragment can be interpolated inside another. The inner SQL is expanded in place and parameters are merged:
216
+
217
+ ```typescript
218
+ const sub = sql`SELECT user_id FROM orders WHERE status = ${'paid'}`;
219
+ // → { sql: 'SELECT user_id FROM orders WHERE status = ?', params: ['paid'] }
220
+
221
+ sql`SELECT * FROM users WHERE id IN (${sub}) AND age > ${18}`;
222
+ // → {
223
+ // sql: 'SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE status = ?) AND age > ?',
224
+ // params: ['paid', 18]
225
+ // }
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Return Types
231
+
232
+ The `sql` tag returns different types based on the interpolated values, enabling type-safe usage in different contexts:
233
+
234
+ | Interpolation Pattern | Return Type | Usage |
235
+ |---|---|---|
236
+ | Single `Column` only | `SqlTypedFragment<V, M>` | Condition tuple first element, or value-free condition (IS NULL) |
237
+ | Single `Column` + value(s) | `SqlCondition<M>` | Direct condition element (Pattern B) |
238
+ | Multiple Columns / TABLE_NAMEs | `SqlFragment` | `QUERY`, `withQuery`, `execute` |
239
+ | Mixed (Columns + values) | `SqlFragment` | `withQuery`, `execute` |
240
+
241
+ - `SqlTypedFragment<V, M>` carries the Column's value type `V` and model type `M`, enabling type checking on the tuple's second element.
242
+ - `SqlCondition<M>` carries the model type `M` for model validation.
243
+ - `SqlFragment` is the general type for arbitrary SQL with parameters.
244
+
245
+ All three types have `sql: string` and `params: readonly unknown[]` properties.
246
+
247
+ ---
248
+
249
+ ## Backward Compatibility
250
+
251
+ The `sql` tag is a purely additive feature. All existing patterns continue to work:
252
+
253
+ | Existing Pattern | Status |
254
+ |---|---|
255
+ | `[Column, value]` tuples | Recommended for equality conditions |
256
+ | `[string, value]` tuples (template literals) | Supported (backward compatible) |
257
+ | `{ sql, params }` for `withQuery` | Supported (backward compatible) |
258
+ | `string` for `QUERY` | Supported (backward compatible) |
259
+ | `execute(sql, params)` | Supported (backward compatible) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "litedbmodel",
3
- "version": "0.22.3",
3
+ "version": "1.0.1",
4
4
  "description": "A lightweight TypeScript ORM for PostgreSQL, MySQL, and SQLite with Active Record pattern",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",