@basicbenframework/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/.github/workflows/publish.yml +35 -0
  2. package/README.md +588 -0
  3. package/bin/cli.js +8 -0
  4. package/create-basicben-app/index.js +205 -0
  5. package/create-basicben-app/package.json +30 -0
  6. package/create-basicben-app/template/.env.example +24 -0
  7. package/create-basicben-app/template/README.md +59 -0
  8. package/create-basicben-app/template/basicben.config.js +33 -0
  9. package/create-basicben-app/template/index.html +54 -0
  10. package/create-basicben-app/template/migrations/001_create_users.js +15 -0
  11. package/create-basicben-app/template/migrations/002_create_posts.js +18 -0
  12. package/create-basicben-app/template/public/.gitkeep +0 -0
  13. package/create-basicben-app/template/seeds/01_users.js +29 -0
  14. package/create-basicben-app/template/seeds/02_posts.js +43 -0
  15. package/create-basicben-app/template/src/client/components/Alert.jsx +11 -0
  16. package/create-basicben-app/template/src/client/components/Avatar.jsx +11 -0
  17. package/create-basicben-app/template/src/client/components/BackLink.jsx +10 -0
  18. package/create-basicben-app/template/src/client/components/Button.jsx +19 -0
  19. package/create-basicben-app/template/src/client/components/Card.jsx +10 -0
  20. package/create-basicben-app/template/src/client/components/Empty.jsx +6 -0
  21. package/create-basicben-app/template/src/client/components/Input.jsx +12 -0
  22. package/create-basicben-app/template/src/client/components/Loading.jsx +6 -0
  23. package/create-basicben-app/template/src/client/components/Logo.jsx +40 -0
  24. package/create-basicben-app/template/src/client/components/Nav/DarkModeToggle.jsx +23 -0
  25. package/create-basicben-app/template/src/client/components/Nav/DesktopNav.jsx +32 -0
  26. package/create-basicben-app/template/src/client/components/Nav/MobileNav.jsx +107 -0
  27. package/create-basicben-app/template/src/client/components/NavLink.jsx +10 -0
  28. package/create-basicben-app/template/src/client/components/PageHeader.jsx +8 -0
  29. package/create-basicben-app/template/src/client/components/PostCard.jsx +19 -0
  30. package/create-basicben-app/template/src/client/components/Textarea.jsx +12 -0
  31. package/create-basicben-app/template/src/client/components/ThemeContext.jsx +5 -0
  32. package/create-basicben-app/template/src/client/contexts/ToastContext.jsx +94 -0
  33. package/create-basicben-app/template/src/client/layouts/AppLayout.jsx +60 -0
  34. package/create-basicben-app/template/src/client/layouts/AuthLayout.jsx +33 -0
  35. package/create-basicben-app/template/src/client/layouts/DocsLayout.jsx +60 -0
  36. package/create-basicben-app/template/src/client/layouts/RootLayout.jsx +25 -0
  37. package/create-basicben-app/template/src/client/pages/Auth.jsx +55 -0
  38. package/create-basicben-app/template/src/client/pages/Authentication.jsx +236 -0
  39. package/create-basicben-app/template/src/client/pages/Database.jsx +426 -0
  40. package/create-basicben-app/template/src/client/pages/Feed.jsx +34 -0
  41. package/create-basicben-app/template/src/client/pages/FeedPost.jsx +37 -0
  42. package/create-basicben-app/template/src/client/pages/GettingStarted.jsx +136 -0
  43. package/create-basicben-app/template/src/client/pages/Home.jsx +206 -0
  44. package/create-basicben-app/template/src/client/pages/PostForm.jsx +69 -0
  45. package/create-basicben-app/template/src/client/pages/Posts.jsx +59 -0
  46. package/create-basicben-app/template/src/client/pages/Profile.jsx +68 -0
  47. package/create-basicben-app/template/src/client/pages/Routing.jsx +207 -0
  48. package/create-basicben-app/template/src/client/pages/Testing.jsx +251 -0
  49. package/create-basicben-app/template/src/client/pages/Validation.jsx +210 -0
  50. package/create-basicben-app/template/src/controllers/AuthController.js +81 -0
  51. package/create-basicben-app/template/src/controllers/HomeController.js +17 -0
  52. package/create-basicben-app/template/src/controllers/PostController.js +86 -0
  53. package/create-basicben-app/template/src/controllers/ProfileController.js +66 -0
  54. package/create-basicben-app/template/src/helpers/api.js +24 -0
  55. package/create-basicben-app/template/src/main.jsx +9 -0
  56. package/create-basicben-app/template/src/middleware/auth.js +16 -0
  57. package/create-basicben-app/template/src/models/Post.js +63 -0
  58. package/create-basicben-app/template/src/models/User.js +42 -0
  59. package/create-basicben-app/template/src/routes/App.jsx +38 -0
  60. package/create-basicben-app/template/src/routes/api/auth.js +7 -0
  61. package/create-basicben-app/template/src/routes/api/posts.js +15 -0
  62. package/create-basicben-app/template/src/routes/api/profile.js +8 -0
  63. package/create-basicben-app/template/src/server/index.js +16 -0
  64. package/create-basicben-app/template/vite.config.js +18 -0
  65. package/database.sqlite +0 -0
  66. package/my-test-app/.env.example +24 -0
  67. package/my-test-app/README.md +59 -0
  68. package/my-test-app/basicben.config.js +33 -0
  69. package/my-test-app/database.sqlite-shm +0 -0
  70. package/my-test-app/database.sqlite-wal +0 -0
  71. package/my-test-app/index.html +54 -0
  72. package/my-test-app/migrations/001_create_users.js +15 -0
  73. package/my-test-app/migrations/002_create_posts.js +18 -0
  74. package/my-test-app/package-lock.json +2160 -0
  75. package/my-test-app/package.json +29 -0
  76. package/my-test-app/public/.gitkeep +0 -0
  77. package/my-test-app/seeds/01_users.js +29 -0
  78. package/my-test-app/seeds/02_posts.js +43 -0
  79. package/my-test-app/src/client/components/Alert.jsx +11 -0
  80. package/my-test-app/src/client/components/Avatar.jsx +11 -0
  81. package/my-test-app/src/client/components/BackLink.jsx +10 -0
  82. package/my-test-app/src/client/components/Button.jsx +19 -0
  83. package/my-test-app/src/client/components/Card.jsx +10 -0
  84. package/my-test-app/src/client/components/Empty.jsx +6 -0
  85. package/my-test-app/src/client/components/Input.jsx +12 -0
  86. package/my-test-app/src/client/components/Loading.jsx +6 -0
  87. package/my-test-app/src/client/components/Logo.jsx +40 -0
  88. package/my-test-app/src/client/components/Nav/DarkModeToggle.jsx +23 -0
  89. package/my-test-app/src/client/components/Nav/DesktopNav.jsx +32 -0
  90. package/my-test-app/src/client/components/Nav/MobileNav.jsx +107 -0
  91. package/my-test-app/src/client/components/NavLink.jsx +10 -0
  92. package/my-test-app/src/client/components/PageHeader.jsx +8 -0
  93. package/my-test-app/src/client/components/PostCard.jsx +19 -0
  94. package/my-test-app/src/client/components/Textarea.jsx +12 -0
  95. package/my-test-app/src/client/components/ThemeContext.jsx +5 -0
  96. package/my-test-app/src/client/contexts/AppContext.jsx +13 -0
  97. package/my-test-app/src/client/contexts/ToastContext.jsx +94 -0
  98. package/my-test-app/src/client/layouts/AppLayout.jsx +60 -0
  99. package/my-test-app/src/client/layouts/AuthLayout.jsx +33 -0
  100. package/my-test-app/src/client/layouts/DocsLayout.jsx +60 -0
  101. package/my-test-app/src/client/layouts/RootLayout.jsx +25 -0
  102. package/my-test-app/src/client/pages/Auth.jsx +55 -0
  103. package/my-test-app/src/client/pages/Authentication.jsx +236 -0
  104. package/my-test-app/src/client/pages/Database.jsx +426 -0
  105. package/my-test-app/src/client/pages/Feed.jsx +34 -0
  106. package/my-test-app/src/client/pages/FeedPost.jsx +37 -0
  107. package/my-test-app/src/client/pages/GettingStarted.jsx +136 -0
  108. package/my-test-app/src/client/pages/Home.jsx +206 -0
  109. package/my-test-app/src/client/pages/PostForm.jsx +69 -0
  110. package/my-test-app/src/client/pages/Posts.jsx +59 -0
  111. package/my-test-app/src/client/pages/Profile.jsx +68 -0
  112. package/my-test-app/src/client/pages/Routing.jsx +207 -0
  113. package/my-test-app/src/client/pages/Testing.jsx +251 -0
  114. package/my-test-app/src/client/pages/Validation.jsx +210 -0
  115. package/my-test-app/src/controllers/AuthController.js +81 -0
  116. package/my-test-app/src/controllers/HomeController.js +17 -0
  117. package/my-test-app/src/controllers/PostController.js +86 -0
  118. package/my-test-app/src/controllers/ProfileController.js +66 -0
  119. package/my-test-app/src/helpers/api.js +24 -0
  120. package/my-test-app/src/main.jsx +9 -0
  121. package/my-test-app/src/middleware/auth.js +16 -0
  122. package/my-test-app/src/models/Post.js +63 -0
  123. package/my-test-app/src/models/User.js +42 -0
  124. package/my-test-app/src/routes/App.jsx +38 -0
  125. package/my-test-app/src/routes/api/auth.js +7 -0
  126. package/my-test-app/src/routes/api/posts.js +15 -0
  127. package/my-test-app/src/routes/api/profile.js +8 -0
  128. package/my-test-app/src/server/index.js +16 -0
  129. package/my-test-app/vite.config.js +18 -0
  130. package/package.json +61 -0
  131. package/scripts/test-app.sh +59 -0
  132. package/src/auth/jwt.js +195 -0
  133. package/src/auth/password.js +132 -0
  134. package/src/cli/colors.js +31 -0
  135. package/src/cli/dispatcher.js +168 -0
  136. package/src/cli/parser.js +91 -0
  137. package/src/client/context.js +4 -0
  138. package/src/client/hooks.js +50 -0
  139. package/src/client/index.js +3 -0
  140. package/src/client/router.js +184 -0
  141. package/src/commands/build.js +155 -0
  142. package/src/commands/dev.js +206 -0
  143. package/src/commands/help.js +84 -0
  144. package/src/commands/make-controller.js +36 -0
  145. package/src/commands/make-middleware.js +44 -0
  146. package/src/commands/make-migration.js +51 -0
  147. package/src/commands/make-model.js +38 -0
  148. package/src/commands/make-route.js +36 -0
  149. package/src/commands/make-seed.js +38 -0
  150. package/src/commands/migrate-fresh.js +32 -0
  151. package/src/commands/migrate-rollback.js +30 -0
  152. package/src/commands/migrate-status.js +41 -0
  153. package/src/commands/migrate.js +30 -0
  154. package/src/commands/seed.js +47 -0
  155. package/src/commands/start.js +69 -0
  156. package/src/commands/test.js +46 -0
  157. package/src/db/Grammar.js +125 -0
  158. package/src/db/QueryBuilder.js +476 -0
  159. package/src/db/adapters/neon.js +170 -0
  160. package/src/db/adapters/planetscale.js +146 -0
  161. package/src/db/adapters/postgres.js +166 -0
  162. package/src/db/adapters/sqlite.js +125 -0
  163. package/src/db/adapters/turso.js +165 -0
  164. package/src/db/index.js +156 -0
  165. package/src/db/migrator.js +250 -0
  166. package/src/db/seeder.js +124 -0
  167. package/src/index.js +12 -0
  168. package/src/scaffolding/index.js +152 -0
  169. package/src/server/body-parser.js +159 -0
  170. package/src/server/cors.js +63 -0
  171. package/src/server/default-entry.js +13 -0
  172. package/src/server/http.js +221 -0
  173. package/src/server/index.js +168 -0
  174. package/src/server/loader.js +128 -0
  175. package/src/server/router.js +281 -0
  176. package/src/server/static.js +139 -0
  177. package/src/validation/index.js +436 -0
  178. package/src/vite/config.js +49 -0
  179. package/stubs/controller.stub +48 -0
  180. package/stubs/middleware-auth.stub +29 -0
  181. package/stubs/middleware.stub +9 -0
  182. package/stubs/migration.stub +17 -0
  183. package/stubs/model.stub +77 -0
  184. package/stubs/route.stub +13 -0
  185. package/stubs/seed.stub +16 -0
  186. package/stubs/vite.config.stub +18 -0
@@ -0,0 +1,476 @@
1
+ /**
2
+ * Query Builder - fluent API for building safe SQL queries.
3
+ * Provides mass assignment protection and identifier escaping.
4
+ */
5
+
6
+ import { Grammar } from './Grammar.js'
7
+
8
+ export class QueryBuilder {
9
+ /**
10
+ * Create a new QueryBuilder instance.
11
+ *
12
+ * @param {Object} db - Database adapter instance
13
+ * @param {string} table - Table name
14
+ * @param {string} driver - Database driver ('sqlite' or 'postgres')
15
+ */
16
+ constructor(db, table, driver = 'sqlite') {
17
+ this.db = db
18
+ this.grammar = new Grammar(driver)
19
+
20
+ // Validate and store table name
21
+ this.grammar.validateId(table)
22
+ this.table = table
23
+
24
+ // Mass assignment protection
25
+ this._fillable = null // Whitelist (null = allow all except guarded)
26
+ this._guarded = ['id'] // Blacklist (always protected by default)
27
+
28
+ // Query state
29
+ this._select = ['*']
30
+ this._wheres = []
31
+ this._orderBy = []
32
+ this._limit = null
33
+ this._offset = null
34
+ this._params = []
35
+ }
36
+
37
+ /**
38
+ * Set fillable columns (whitelist).
39
+ * Only these columns will be allowed in insert/update operations.
40
+ *
41
+ * @param {...string} columns - Column names to allow
42
+ * @returns {QueryBuilder}
43
+ */
44
+ only(...columns) {
45
+ this._fillable = columns.flat()
46
+ return this
47
+ }
48
+
49
+ /**
50
+ * Set guarded columns (blacklist).
51
+ * These columns will be excluded from insert/update operations.
52
+ *
53
+ * @param {...string} columns - Column names to exclude
54
+ * @returns {QueryBuilder}
55
+ */
56
+ except(...columns) {
57
+ this._guarded = columns.flat()
58
+ return this
59
+ }
60
+
61
+ /**
62
+ * Filter data object to only allowed columns.
63
+ * Validates identifiers and applies fillable/guarded rules.
64
+ *
65
+ * @param {Object} data - Data object to filter
66
+ * @returns {Object} Filtered data
67
+ * @throws {Error} If an invalid identifier is found
68
+ */
69
+ filterData(data) {
70
+ if (!data || typeof data !== 'object') {
71
+ throw new Error('Data must be an object')
72
+ }
73
+
74
+ const filtered = {}
75
+
76
+ for (const [key, value] of Object.entries(data)) {
77
+ // Validate identifier (throws if invalid)
78
+ this.grammar.validateId(key)
79
+
80
+ // Check fillable whitelist
81
+ if (this._fillable !== null && !this._fillable.includes(key)) {
82
+ continue
83
+ }
84
+
85
+ // Check guarded blacklist
86
+ if (this._guarded.includes(key)) {
87
+ continue
88
+ }
89
+
90
+ filtered[key] = value
91
+ }
92
+
93
+ return filtered
94
+ }
95
+
96
+ /**
97
+ * Set columns to select.
98
+ *
99
+ * @param {...string} columns - Column names
100
+ * @returns {QueryBuilder}
101
+ */
102
+ select(...columns) {
103
+ const cols = columns.flat()
104
+
105
+ this._select = cols.map(col => {
106
+ if (col === '*') return '*'
107
+ return this.grammar.escapeId(col)
108
+ })
109
+
110
+ return this
111
+ }
112
+
113
+ /**
114
+ * Add a WHERE clause.
115
+ *
116
+ * @param {string} column - Column name
117
+ * @param {string} [operator='='] - Comparison operator
118
+ * @param {*} value - Value to compare
119
+ * @returns {QueryBuilder}
120
+ */
121
+ where(column, operator, value) {
122
+ // Support where(column, value) shorthand
123
+ if (value === undefined) {
124
+ value = operator
125
+ operator = '='
126
+ }
127
+
128
+ this.grammar.validateId(column)
129
+ const validOp = this.grammar.validateOperator(operator)
130
+
131
+ this._wheres.push({
132
+ column,
133
+ operator: validOp,
134
+ paramIndex: this._params.length
135
+ })
136
+
137
+ this._params.push(value)
138
+
139
+ return this
140
+ }
141
+
142
+ /**
143
+ * Add a WHERE NULL clause.
144
+ *
145
+ * @param {string} column - Column name
146
+ * @returns {QueryBuilder}
147
+ */
148
+ whereNull(column) {
149
+ this.grammar.validateId(column)
150
+
151
+ this._wheres.push({
152
+ column,
153
+ operator: 'IS',
154
+ value: 'NULL',
155
+ raw: true
156
+ })
157
+
158
+ return this
159
+ }
160
+
161
+ /**
162
+ * Add a WHERE NOT NULL clause.
163
+ *
164
+ * @param {string} column - Column name
165
+ * @returns {QueryBuilder}
166
+ */
167
+ whereNotNull(column) {
168
+ this.grammar.validateId(column)
169
+
170
+ this._wheres.push({
171
+ column,
172
+ operator: 'IS NOT',
173
+ value: 'NULL',
174
+ raw: true
175
+ })
176
+
177
+ return this
178
+ }
179
+
180
+ /**
181
+ * Add ORDER BY clause.
182
+ *
183
+ * @param {string} column - Column name
184
+ * @param {string} [direction='ASC'] - Sort direction
185
+ * @returns {QueryBuilder}
186
+ */
187
+ orderBy(column, direction = 'ASC') {
188
+ this.grammar.validateId(column)
189
+ const dir = this.grammar.validateDirection(direction)
190
+
191
+ this._orderBy.push({ column, direction: dir })
192
+
193
+ return this
194
+ }
195
+
196
+ /**
197
+ * Set LIMIT.
198
+ *
199
+ * @param {number} n - Number of rows
200
+ * @returns {QueryBuilder}
201
+ */
202
+ limit(n) {
203
+ const num = parseInt(n, 10)
204
+ if (isNaN(num) || num < 0) {
205
+ throw new Error('Limit must be a non-negative integer')
206
+ }
207
+ this._limit = num
208
+ return this
209
+ }
210
+
211
+ /**
212
+ * Set OFFSET.
213
+ *
214
+ * @param {number} n - Number of rows to skip
215
+ * @returns {QueryBuilder}
216
+ */
217
+ offset(n) {
218
+ const num = parseInt(n, 10)
219
+ if (isNaN(num) || num < 0) {
220
+ throw new Error('Offset must be a non-negative integer')
221
+ }
222
+ this._offset = num
223
+ return this
224
+ }
225
+
226
+ /**
227
+ * Build the WHERE clause SQL.
228
+ *
229
+ * @returns {{ sql: string, startIndex: number }}
230
+ */
231
+ _buildWhereClause(startIndex = 0) {
232
+ if (this._wheres.length === 0) {
233
+ return { sql: '', startIndex }
234
+ }
235
+
236
+ const clauses = this._wheres.map((w, i) => {
237
+ const col = this.grammar.escapeId(w.column)
238
+
239
+ if (w.raw) {
240
+ return `${col} ${w.operator} ${w.value}`
241
+ }
242
+
243
+ const placeholder = this.grammar.placeholder(startIndex + i)
244
+ return `${col} ${w.operator} ${placeholder}`
245
+ })
246
+
247
+ return {
248
+ sql: ` WHERE ${clauses.join(' AND ')}`,
249
+ startIndex: startIndex + this._wheres.filter(w => !w.raw).length
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Build SELECT query SQL.
255
+ *
256
+ * @returns {string}
257
+ */
258
+ toSql() {
259
+ const table = this.grammar.escapeId(this.table)
260
+ let sql = `SELECT ${this._select.join(', ')} FROM ${table}`
261
+
262
+ // WHERE
263
+ const { sql: whereClause } = this._buildWhereClause(0)
264
+ sql += whereClause
265
+
266
+ // ORDER BY
267
+ if (this._orderBy.length > 0) {
268
+ const orders = this._orderBy.map(o =>
269
+ `${this.grammar.escapeId(o.column)} ${o.direction}`
270
+ )
271
+ sql += ` ORDER BY ${orders.join(', ')}`
272
+ }
273
+
274
+ // LIMIT
275
+ if (this._limit !== null) {
276
+ sql += ` LIMIT ${this._limit}`
277
+ }
278
+
279
+ // OFFSET
280
+ if (this._offset !== null) {
281
+ sql += ` OFFSET ${this._offset}`
282
+ }
283
+
284
+ return sql
285
+ }
286
+
287
+ /**
288
+ * Get the parameters for the current query.
289
+ *
290
+ * @returns {Array}
291
+ */
292
+ getParams() {
293
+ return this._wheres.filter(w => !w.raw).map(w => this._params[w.paramIndex])
294
+ }
295
+
296
+ /**
297
+ * Execute SELECT and return all rows.
298
+ *
299
+ * @returns {Promise<Array>}
300
+ */
301
+ async get() {
302
+ const sql = this.toSql()
303
+ const params = this.getParams()
304
+ return this.db.all(sql, params)
305
+ }
306
+
307
+ /**
308
+ * Execute SELECT and return first row.
309
+ *
310
+ * @returns {Promise<Object|undefined>}
311
+ */
312
+ async first() {
313
+ this._limit = 1
314
+ const sql = this.toSql()
315
+ const params = this.getParams()
316
+ return this.db.get(sql, params)
317
+ }
318
+
319
+ /**
320
+ * Find a record by ID.
321
+ *
322
+ * @param {number|string} id - The ID to find
323
+ * @returns {Promise<Object|undefined>}
324
+ */
325
+ async find(id) {
326
+ return this.where('id', id).first()
327
+ }
328
+
329
+ /**
330
+ * Execute INSERT.
331
+ *
332
+ * @param {Object} data - Data to insert
333
+ * @returns {Promise<{ lastInsertRowid: number, changes: number }>}
334
+ */
335
+ async insert(data) {
336
+ const filtered = this.filterData(data)
337
+ const keys = Object.keys(filtered)
338
+ const values = Object.values(filtered)
339
+
340
+ if (keys.length === 0) {
341
+ throw new Error('No valid columns to insert')
342
+ }
343
+
344
+ const table = this.grammar.escapeId(this.table)
345
+ const columns = keys.map(k => this.grammar.escapeId(k)).join(', ')
346
+ const placeholders = keys.map((_, i) => this.grammar.placeholder(i)).join(', ')
347
+
348
+ const sql = `INSERT INTO ${table} (${columns}) VALUES (${placeholders})`
349
+
350
+ return this.db.run(sql, values)
351
+ }
352
+
353
+ /**
354
+ * Execute UPDATE.
355
+ *
356
+ * @param {Object} data - Data to update
357
+ * @returns {Promise<{ lastInsertRowid: number, changes: number }>}
358
+ */
359
+ async update(data) {
360
+ const filtered = this.filterData(data)
361
+ const keys = Object.keys(filtered)
362
+ const values = Object.values(filtered)
363
+
364
+ if (keys.length === 0) {
365
+ throw new Error('No valid columns to update')
366
+ }
367
+
368
+ const table = this.grammar.escapeId(this.table)
369
+ const whereParams = this.getParams()
370
+
371
+ // Build SET clause with correct placeholder indices
372
+ const setClause = keys.map((k, i) => {
373
+ const placeholder = this.grammar.placeholder(i)
374
+ return `${this.grammar.escapeId(k)} = ${placeholder}`
375
+ }).join(', ')
376
+
377
+ let sql = `UPDATE ${table} SET ${setClause}`
378
+
379
+ // WHERE clause with offset indices
380
+ if (this._wheres.length > 0) {
381
+ const clauses = this._wheres.map((w, i) => {
382
+ if (w.raw) {
383
+ return `${this.grammar.escapeId(w.column)} ${w.operator} ${w.value}`
384
+ }
385
+ const placeholder = this.grammar.placeholder(keys.length + i)
386
+ return `${this.grammar.escapeId(w.column)} ${w.operator} ${placeholder}`
387
+ })
388
+ sql += ` WHERE ${clauses.join(' AND ')}`
389
+ }
390
+
391
+ return this.db.run(sql, [...values, ...whereParams])
392
+ }
393
+
394
+ /**
395
+ * Execute DELETE.
396
+ *
397
+ * @returns {Promise<{ lastInsertRowid: number, changes: number }>}
398
+ */
399
+ async delete() {
400
+ const table = this.grammar.escapeId(this.table)
401
+ let sql = `DELETE FROM ${table}`
402
+
403
+ const { sql: whereClause } = this._buildWhereClause(0)
404
+ sql += whereClause
405
+
406
+ return this.db.run(sql, this.getParams())
407
+ }
408
+
409
+ /**
410
+ * Get COUNT of matching rows.
411
+ *
412
+ * @returns {Promise<number>}
413
+ */
414
+ async count() {
415
+ const table = this.grammar.escapeId(this.table)
416
+ let sql = `SELECT COUNT(*) as count FROM ${table}`
417
+
418
+ const { sql: whereClause } = this._buildWhereClause(0)
419
+ sql += whereClause
420
+
421
+ const result = await this.db.get(sql, this.getParams())
422
+ return result?.count || 0
423
+ }
424
+
425
+ /**
426
+ * Check if any matching rows exist.
427
+ *
428
+ * @returns {Promise<boolean>}
429
+ */
430
+ async exists() {
431
+ const count = await this.count()
432
+ return count > 0
433
+ }
434
+
435
+ /**
436
+ * Paginate results.
437
+ *
438
+ * @param {number} page - Page number (1-indexed)
439
+ * @param {number} perPage - Items per page
440
+ * @returns {Promise<{ data: Array, total: number, page: number, perPage: number, totalPages: number }>}
441
+ */
442
+ async paginate(page = 1, perPage = 15) {
443
+ const pageNum = Math.max(1, parseInt(page, 10))
444
+ const perPageNum = Math.max(1, parseInt(perPage, 10))
445
+
446
+ // Get total count (clone query state)
447
+ const total = await this.count()
448
+
449
+ // Get paginated data
450
+ this._limit = perPageNum
451
+ this._offset = (pageNum - 1) * perPageNum
452
+
453
+ const data = await this.get()
454
+
455
+ return {
456
+ data,
457
+ total,
458
+ page: pageNum,
459
+ perPage: perPageNum,
460
+ totalPages: Math.ceil(total / perPageNum)
461
+ }
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Create a new QueryBuilder instance.
467
+ * Factory function for cleaner syntax.
468
+ *
469
+ * @param {Object} db - Database adapter
470
+ * @param {string} table - Table name
471
+ * @param {string} driver - Database driver
472
+ * @returns {QueryBuilder}
473
+ */
474
+ export function query(db, table, driver = 'sqlite') {
475
+ return new QueryBuilder(db, table, driver)
476
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Neon adapter using @neondatabase/serverless.
3
+ * Provides serverless Postgres with WebSocket support.
4
+ */
5
+
6
+ let neon = null
7
+
8
+ /**
9
+ * Load @neondatabase/serverless dynamically
10
+ */
11
+ async function loadDriver() {
12
+ if (neon) return neon
13
+
14
+ try {
15
+ neon = await import('@neondatabase/serverless')
16
+ return neon
17
+ } catch {
18
+ throw new Error(
19
+ '@neondatabase/serverless is required for Neon support.\n' +
20
+ 'Install it with: npm install @neondatabase/serverless'
21
+ )
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Create Neon adapter
27
+ *
28
+ * @param {string} url - Neon connection string (postgres://...)
29
+ * @param {Object} options - Additional options
30
+ */
31
+ export async function createNeonAdapter(url, options = {}) {
32
+ const { neon: createNeon, neonConfig } = await loadDriver()
33
+
34
+ // Configure for serverless environment
35
+ if (options.fetchConnectionCache !== undefined) {
36
+ neonConfig.fetchConnectionCache = options.fetchConnectionCache
37
+ }
38
+
39
+ const sql = createNeon(url)
40
+
41
+ return {
42
+ /**
43
+ * Driver name for query builder
44
+ */
45
+ driver: 'neon',
46
+
47
+ /**
48
+ * Run INSERT/UPDATE/DELETE
49
+ */
50
+ async run(sqlStr, params = []) {
51
+ const normalizedParams = normalizeParams(params)
52
+ const result = await sql(sqlStr, normalizedParams)
53
+
54
+ // Try to get lastInsertRowid from RETURNING clause
55
+ let lastInsertRowid = null
56
+ if (result[0] && result[0].id !== undefined) {
57
+ lastInsertRowid = result[0].id
58
+ }
59
+
60
+ return {
61
+ lastInsertRowid,
62
+ changes: result.count || 0
63
+ }
64
+ },
65
+
66
+ /**
67
+ * Get single row
68
+ */
69
+ async get(sqlStr, params = []) {
70
+ const normalizedParams = normalizeParams(params)
71
+ const result = await sql(sqlStr, normalizedParams)
72
+ return result[0] || undefined
73
+ },
74
+
75
+ /**
76
+ * Get all rows
77
+ */
78
+ async all(sqlStr, params = []) {
79
+ const normalizedParams = normalizeParams(params)
80
+ return sql(sqlStr, normalizedParams)
81
+ },
82
+
83
+ /**
84
+ * Execute raw SQL
85
+ */
86
+ async exec(sqlStr) {
87
+ // Split by semicolon and execute each statement
88
+ const statements = sqlStr
89
+ .split(';')
90
+ .map(s => s.trim())
91
+ .filter(s => s.length > 0)
92
+
93
+ for (const statement of statements) {
94
+ await sql(statement)
95
+ }
96
+ },
97
+
98
+ /**
99
+ * Run function in transaction
100
+ */
101
+ async transaction(fn) {
102
+ const { Pool } = await loadDriver()
103
+ const pool = new Pool({ connectionString: url })
104
+ const client = await pool.connect()
105
+
106
+ try {
107
+ await client.query('BEGIN')
108
+
109
+ const txAdapter = {
110
+ async run(sqlStr, params = []) {
111
+ const result = await client.query(sqlStr, normalizeParams(params))
112
+ let lastInsertRowid = null
113
+ if (result.rows && result.rows[0] && result.rows[0].id !== undefined) {
114
+ lastInsertRowid = result.rows[0].id
115
+ }
116
+ return { lastInsertRowid, changes: result.rowCount }
117
+ },
118
+ async get(sqlStr, params = []) {
119
+ const result = await client.query(sqlStr, normalizeParams(params))
120
+ return result.rows[0]
121
+ },
122
+ async all(sqlStr, params = []) {
123
+ const result = await client.query(sqlStr, normalizeParams(params))
124
+ return result.rows
125
+ },
126
+ async exec(sqlStr) {
127
+ await client.query(sqlStr)
128
+ }
129
+ }
130
+
131
+ const result = await fn(txAdapter)
132
+ await client.query('COMMIT')
133
+ return result
134
+ } catch (err) {
135
+ await client.query('ROLLBACK')
136
+ throw err
137
+ } finally {
138
+ client.release()
139
+ await pool.end()
140
+ }
141
+ },
142
+
143
+ /**
144
+ * Close connection (no-op for serverless)
145
+ */
146
+ async close() {
147
+ // Serverless connections are stateless
148
+ },
149
+
150
+ /**
151
+ * Get underlying sql function
152
+ */
153
+ get raw() {
154
+ return sql
155
+ }
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Normalize params to array format
161
+ */
162
+ function normalizeParams(params) {
163
+ if (Array.isArray(params)) {
164
+ return params
165
+ }
166
+ if (params === undefined || params === null) {
167
+ return []
168
+ }
169
+ return [params]
170
+ }