@buenojs/bueno 0.8.4 → 0.8.6

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 (234) hide show
  1. package/README.md +264 -17
  2. package/dist/cli/{index.js → bin.js} +413 -332
  3. package/dist/container/index.js +273 -0
  4. package/dist/context/index.js +219 -0
  5. package/dist/database/index.js +493 -0
  6. package/dist/frontend/index.js +7697 -0
  7. package/dist/graphql/index.js +2156 -0
  8. package/dist/health/index.js +364 -0
  9. package/dist/i18n/index.js +345 -0
  10. package/dist/index.js +9694 -5047
  11. package/dist/jobs/index.js +819 -0
  12. package/dist/lock/index.js +367 -0
  13. package/dist/logger/index.js +281 -0
  14. package/dist/metrics/index.js +289 -0
  15. package/dist/middleware/index.js +77 -0
  16. package/dist/migrations/index.js +571 -0
  17. package/dist/modules/index.js +3411 -0
  18. package/dist/notification/index.js +484 -0
  19. package/dist/observability/index.js +331 -0
  20. package/dist/openapi/index.js +795 -0
  21. package/dist/orm/index.js +1356 -0
  22. package/dist/router/index.js +886 -0
  23. package/dist/rpc/index.js +691 -0
  24. package/dist/schema/index.js +400 -0
  25. package/dist/telemetry/index.js +595 -0
  26. package/dist/template/index.js +640 -0
  27. package/dist/templates/index.js +640 -0
  28. package/dist/testing/index.js +1111 -0
  29. package/dist/types/index.js +60 -0
  30. package/llms.txt +231 -0
  31. package/package.json +125 -27
  32. package/src/cache/index.ts +2 -1
  33. package/src/cli/ARCHITECTURE.md +3 -3
  34. package/src/cli/bin.ts +2 -2
  35. package/src/cli/commands/build.ts +183 -165
  36. package/src/cli/commands/dev.ts +96 -89
  37. package/src/cli/commands/generate.ts +142 -111
  38. package/src/cli/commands/help.ts +20 -16
  39. package/src/cli/commands/index.ts +3 -6
  40. package/src/cli/commands/migration.ts +124 -105
  41. package/src/cli/commands/new.ts +294 -232
  42. package/src/cli/commands/start.ts +81 -79
  43. package/src/cli/core/args.ts +68 -50
  44. package/src/cli/core/console.ts +89 -95
  45. package/src/cli/core/index.ts +4 -4
  46. package/src/cli/core/prompt.ts +65 -62
  47. package/src/cli/core/spinner.ts +23 -20
  48. package/src/cli/index.ts +46 -38
  49. package/src/cli/templates/database/index.ts +37 -18
  50. package/src/cli/templates/database/mysql.ts +3 -3
  51. package/src/cli/templates/database/none.ts +2 -2
  52. package/src/cli/templates/database/postgresql.ts +3 -3
  53. package/src/cli/templates/database/sqlite.ts +3 -3
  54. package/src/cli/templates/deploy.ts +29 -26
  55. package/src/cli/templates/docker.ts +41 -30
  56. package/src/cli/templates/frontend/index.ts +33 -15
  57. package/src/cli/templates/frontend/none.ts +2 -2
  58. package/src/cli/templates/frontend/react.ts +18 -18
  59. package/src/cli/templates/frontend/solid.ts +15 -15
  60. package/src/cli/templates/frontend/svelte.ts +17 -17
  61. package/src/cli/templates/frontend/vue.ts +15 -15
  62. package/src/cli/templates/generators/index.ts +29 -29
  63. package/src/cli/templates/generators/types.ts +21 -21
  64. package/src/cli/templates/index.ts +6 -6
  65. package/src/cli/templates/project/api.ts +37 -36
  66. package/src/cli/templates/project/default.ts +25 -25
  67. package/src/cli/templates/project/fullstack.ts +28 -26
  68. package/src/cli/templates/project/index.ts +55 -16
  69. package/src/cli/templates/project/minimal.ts +17 -12
  70. package/src/cli/templates/project/types.ts +10 -5
  71. package/src/cli/templates/project/website.ts +15 -15
  72. package/src/cli/utils/fs.ts +55 -41
  73. package/src/cli/utils/index.ts +3 -3
  74. package/src/cli/utils/strings.ts +47 -33
  75. package/src/cli/utils/version.ts +14 -8
  76. package/src/config/env-validation.ts +100 -0
  77. package/src/config/env.ts +169 -41
  78. package/src/config/index.ts +28 -20
  79. package/src/config/loader.ts +25 -16
  80. package/src/config/merge.ts +21 -10
  81. package/src/config/types.ts +566 -25
  82. package/src/config/validation.ts +215 -7
  83. package/src/container/forward-ref.ts +22 -22
  84. package/src/container/index.ts +34 -12
  85. package/src/context/index.ts +11 -1
  86. package/src/database/index.ts +7 -190
  87. package/src/database/orm/builder.ts +457 -0
  88. package/src/database/orm/casts/index.ts +130 -0
  89. package/src/database/orm/casts/types.ts +25 -0
  90. package/src/database/orm/compiler.ts +304 -0
  91. package/src/database/orm/hooks/index.ts +114 -0
  92. package/src/database/orm/index.ts +61 -0
  93. package/src/database/orm/model-registry.ts +59 -0
  94. package/src/database/orm/model.ts +821 -0
  95. package/src/database/orm/relationships/base.ts +146 -0
  96. package/src/database/orm/relationships/belongs-to-many.ts +179 -0
  97. package/src/database/orm/relationships/belongs-to.ts +56 -0
  98. package/src/database/orm/relationships/has-many.ts +45 -0
  99. package/src/database/orm/relationships/has-one.ts +41 -0
  100. package/src/database/orm/relationships/index.ts +11 -0
  101. package/src/database/orm/scopes/index.ts +55 -0
  102. package/src/events/__tests__/event-system.test.ts +235 -0
  103. package/src/events/config.ts +238 -0
  104. package/src/events/example-usage.ts +185 -0
  105. package/src/events/index.ts +278 -0
  106. package/src/events/manager.ts +385 -0
  107. package/src/events/registry.ts +182 -0
  108. package/src/events/types.ts +124 -0
  109. package/src/frontend/api-routes.ts +65 -23
  110. package/src/frontend/bundler.ts +76 -34
  111. package/src/frontend/console-client.ts +2 -2
  112. package/src/frontend/console-stream.ts +94 -38
  113. package/src/frontend/dev-server.ts +94 -46
  114. package/src/frontend/file-router.ts +61 -19
  115. package/src/frontend/frameworks/index.ts +37 -10
  116. package/src/frontend/frameworks/react.ts +10 -8
  117. package/src/frontend/frameworks/solid.ts +11 -9
  118. package/src/frontend/frameworks/svelte.ts +15 -9
  119. package/src/frontend/frameworks/vue.ts +13 -11
  120. package/src/frontend/hmr-client.ts +12 -10
  121. package/src/frontend/hmr.ts +146 -103
  122. package/src/frontend/index.ts +14 -5
  123. package/src/frontend/islands.ts +41 -22
  124. package/src/frontend/isr.ts +59 -37
  125. package/src/frontend/layout.ts +36 -21
  126. package/src/frontend/ssr/react.ts +74 -27
  127. package/src/frontend/ssr/solid.ts +54 -20
  128. package/src/frontend/ssr/svelte.ts +48 -14
  129. package/src/frontend/ssr/vue.ts +50 -18
  130. package/src/frontend/ssr.ts +83 -39
  131. package/src/frontend/types.ts +91 -56
  132. package/src/graphql/built-in-engine.ts +598 -0
  133. package/src/graphql/context-builder.ts +110 -0
  134. package/src/graphql/decorators.ts +358 -0
  135. package/src/graphql/execution-pipeline.ts +227 -0
  136. package/src/graphql/graphql-module.ts +563 -0
  137. package/src/graphql/index.ts +101 -0
  138. package/src/graphql/metadata.ts +237 -0
  139. package/src/graphql/schema-builder.ts +319 -0
  140. package/src/graphql/subscription-handler.ts +283 -0
  141. package/src/graphql/types.ts +324 -0
  142. package/src/health/index.ts +21 -9
  143. package/src/i18n/engine.ts +305 -0
  144. package/src/i18n/index.ts +38 -0
  145. package/src/i18n/loader.ts +218 -0
  146. package/src/i18n/middleware.ts +164 -0
  147. package/src/i18n/negotiator.ts +162 -0
  148. package/src/i18n/types.ts +158 -0
  149. package/src/index.ts +182 -27
  150. package/src/jobs/drivers/memory.ts +315 -0
  151. package/src/jobs/drivers/redis.ts +459 -0
  152. package/src/jobs/index.ts +30 -0
  153. package/src/jobs/queue.ts +281 -0
  154. package/src/jobs/types.ts +295 -0
  155. package/src/jobs/worker.ts +380 -0
  156. package/src/logger/index.ts +1 -3
  157. package/src/logger/transports/index.ts +62 -22
  158. package/src/metrics/index.ts +25 -16
  159. package/src/migrations/index.ts +9 -0
  160. package/src/modules/filters.ts +13 -17
  161. package/src/modules/guards.ts +49 -26
  162. package/src/modules/index.ts +457 -299
  163. package/src/modules/interceptors.ts +58 -20
  164. package/src/modules/lazy.ts +11 -19
  165. package/src/modules/lifecycle.ts +15 -7
  166. package/src/modules/metadata.ts +15 -5
  167. package/src/modules/pipes.ts +94 -72
  168. package/src/notification/channels/base.ts +68 -0
  169. package/src/notification/channels/email.ts +105 -0
  170. package/src/notification/channels/push.ts +104 -0
  171. package/src/notification/channels/sms.ts +105 -0
  172. package/src/notification/channels/whatsapp.ts +104 -0
  173. package/src/notification/index.ts +48 -0
  174. package/src/notification/service.ts +354 -0
  175. package/src/notification/types.ts +344 -0
  176. package/src/observability/__tests__/observability.test.ts +483 -0
  177. package/src/observability/breadcrumbs.ts +114 -0
  178. package/src/observability/index.ts +136 -0
  179. package/src/observability/interceptor.ts +85 -0
  180. package/src/observability/service.ts +303 -0
  181. package/src/observability/trace.ts +37 -0
  182. package/src/observability/types.ts +196 -0
  183. package/src/openapi/__tests__/decorators.test.ts +335 -0
  184. package/src/openapi/__tests__/document-builder.test.ts +285 -0
  185. package/src/openapi/__tests__/route-scanner.test.ts +334 -0
  186. package/src/openapi/__tests__/schema-generator.test.ts +275 -0
  187. package/src/openapi/decorators.ts +328 -0
  188. package/src/openapi/document-builder.ts +274 -0
  189. package/src/openapi/index.ts +112 -0
  190. package/src/openapi/metadata.ts +112 -0
  191. package/src/openapi/route-scanner.ts +289 -0
  192. package/src/openapi/schema-generator.ts +256 -0
  193. package/src/openapi/swagger-module.ts +166 -0
  194. package/src/openapi/types.ts +398 -0
  195. package/src/orm/index.ts +10 -0
  196. package/src/rpc/index.ts +3 -1
  197. package/src/schema/index.ts +9 -0
  198. package/src/security/index.ts +15 -6
  199. package/src/ssg/index.ts +9 -8
  200. package/src/telemetry/index.ts +76 -22
  201. package/src/template/index.ts +7 -0
  202. package/src/templates/engine.ts +224 -0
  203. package/src/templates/index.ts +9 -0
  204. package/src/templates/loader.ts +331 -0
  205. package/src/templates/renderers/markdown.ts +212 -0
  206. package/src/templates/renderers/simple.ts +269 -0
  207. package/src/templates/types.ts +154 -0
  208. package/src/testing/index.ts +100 -27
  209. package/src/types/optional-deps.d.ts +347 -187
  210. package/src/validation/index.ts +92 -2
  211. package/src/validation/schemas.ts +536 -0
  212. package/tests/integration/cli.test.ts +19 -19
  213. package/tests/integration/fullstack.test.ts +4 -4
  214. package/tests/unit/cli.test.ts +1 -1
  215. package/tests/unit/database.test.ts +2 -72
  216. package/tests/unit/env-validation.test.ts +166 -0
  217. package/tests/unit/events.test.ts +910 -0
  218. package/tests/unit/graphql.test.ts +991 -0
  219. package/tests/unit/i18n.test.ts +455 -0
  220. package/tests/unit/jobs.test.ts +493 -0
  221. package/tests/unit/notification.test.ts +988 -0
  222. package/tests/unit/observability.test.ts +453 -0
  223. package/tests/unit/orm/builder.test.ts +323 -0
  224. package/tests/unit/orm/casts.test.ts +179 -0
  225. package/tests/unit/orm/compiler.test.ts +220 -0
  226. package/tests/unit/orm/eager-loading.test.ts +285 -0
  227. package/tests/unit/orm/hooks.test.ts +191 -0
  228. package/tests/unit/orm/model.test.ts +373 -0
  229. package/tests/unit/orm/relationships.test.ts +303 -0
  230. package/tests/unit/orm/scopes.test.ts +74 -0
  231. package/tests/unit/templates-simple.test.ts +53 -0
  232. package/tests/unit/templates.test.ts +454 -0
  233. package/tests/unit/validation.test.ts +18 -24
  234. package/tsconfig.json +11 -3
@@ -162,7 +162,9 @@ function buildSetFragment(data: Record<string, unknown>): {
162
162
  /**
163
163
  * Detect operation type from SQL string
164
164
  */
165
- function detectOperationType(sql: string): "query" | "insert" | "update" | "delete" | "other" {
165
+ function detectOperationType(
166
+ sql: string,
167
+ ): "query" | "insert" | "update" | "delete" | "other" {
166
168
  const normalizedSql = sql.trim().toUpperCase();
167
169
  if (normalizedSql.startsWith("SELECT")) return "query";
168
170
  if (normalizedSql.startsWith("INSERT")) return "insert";
@@ -195,7 +197,8 @@ export class Database {
195
197
  };
196
198
 
197
199
  // Event listeners
198
- private eventListeners: Map<QueryEventType, Set<QueryEventListener>> = new Map();
200
+ private eventListeners: Map<QueryEventType, Set<QueryEventListener>> =
201
+ new Map();
199
202
 
200
203
  constructor(config: DatabaseConfig | string) {
201
204
  this.config = typeof config === "string" ? { url: config } : config;
@@ -252,7 +255,8 @@ export class Database {
252
255
 
253
256
  this.metrics.totalOperations++;
254
257
  this.metrics.totalLatency += latency;
255
- this.metrics.avgLatency = this.metrics.totalLatency / this.metrics.totalOperations;
258
+ this.metrics.avgLatency =
259
+ this.metrics.totalLatency / this.metrics.totalOperations;
256
260
 
257
261
  if (latency > this.slowQueryThreshold) {
258
262
  this.metrics.slowQueries++;
@@ -930,189 +934,6 @@ export async function createConnection(
930
934
  return db;
931
935
  }
932
936
 
933
- // ============= Query Builder =============
934
-
935
- /**
936
- * Simple query builder for common operations
937
- */
938
- export class QueryBuilder<T = unknown> {
939
- private db: Database;
940
- private tableName: string;
941
-
942
- constructor(db: Database, tableName: string) {
943
- this.db = db;
944
- this.tableName = tableName;
945
- }
946
-
947
- /**
948
- * Select all rows
949
- */
950
- async all(): Promise<T[]> {
951
- return this.db.raw<T>(`SELECT * FROM ${this.tableName}`);
952
- }
953
-
954
- /**
955
- * Find by ID
956
- */
957
- async findById(id: number | string): Promise<T | null> {
958
- const results = await this.db.raw<T>(
959
- `SELECT * FROM ${this.tableName} WHERE id = $1`,
960
- [id],
961
- );
962
- return results.length > 0 ? results[0] : null;
963
- }
964
-
965
- /**
966
- * Find by field
967
- */
968
- async findBy(field: string, value: unknown): Promise<T[]> {
969
- // Note: Field name needs to be safely inserted
970
- const sql = `SELECT * FROM ${this.tableName} WHERE ${field} = $1`;
971
- return this.db.raw<T>(sql, [value]);
972
- }
973
-
974
- /**
975
- * Find one by field
976
- */
977
- async findOneBy(field: string, value: unknown): Promise<T | null> {
978
- const results = await this.findBy(field, value);
979
- return results.length > 0 ? results[0] : null;
980
- }
981
-
982
- /**
983
- * Insert a row
984
- */
985
- async insert(data: Partial<T>): Promise<T> {
986
- const keys = Object.keys(data);
987
- const values = Object.values(data);
988
-
989
- const columns = keys.join(", ");
990
- const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
991
-
992
- const result = await this.db.raw<T>(
993
- `INSERT INTO ${this.tableName} (${columns}) VALUES (${placeholders}) RETURNING *`,
994
- values,
995
- );
996
-
997
- return result[0];
998
- }
999
-
1000
- /**
1001
- * Bulk insert
1002
- */
1003
- async insertMany(items: Partial<T>[]): Promise<T[]> {
1004
- if (items.length === 0) return [];
1005
-
1006
- const results: T[] = [];
1007
-
1008
- for (const item of items) {
1009
- const result = await this.insert(item);
1010
- results.push(result);
1011
- }
1012
-
1013
- return results;
1014
- }
1015
-
1016
- /**
1017
- * Update by ID
1018
- */
1019
- async updateById(id: number | string, data: Partial<T>): Promise<T | null> {
1020
- const keys = Object.keys(data);
1021
- const values = Object.values(data);
1022
-
1023
- const setClause = keys.map((k, i) => `${k} = $${i + 1}`).join(", ");
1024
-
1025
- const result = await this.db.raw<T>(
1026
- `UPDATE ${this.tableName} SET ${setClause} WHERE id = $${keys.length + 1} RETURNING *`,
1027
- [...values, id],
1028
- );
1029
-
1030
- return result.length > 0 ? result[0] : null;
1031
- }
1032
-
1033
- /**
1034
- * Delete by ID
1035
- */
1036
- async deleteById(id: number | string): Promise<boolean> {
1037
- const result = await this.db.raw(
1038
- `DELETE FROM ${this.tableName} WHERE id = $1 RETURNING id`,
1039
- [id],
1040
- );
1041
- return result.length > 0;
1042
- }
1043
-
1044
- /**
1045
- * Count rows
1046
- */
1047
- async count(where?: string, params: unknown[] = []): Promise<number> {
1048
- const sql = where
1049
- ? `SELECT COUNT(*) as count FROM ${this.tableName} WHERE ${where}`
1050
- : `SELECT COUNT(*) as count FROM ${this.tableName}`;
1051
-
1052
- const result = await this.db.raw<{ count: number | string }>(sql, params);
1053
- return Number(result[0]?.count ?? 0);
1054
- }
1055
-
1056
- /**
1057
- * Check if exists
1058
- */
1059
- async exists(where: string, params: unknown[] = []): Promise<boolean> {
1060
- const count = await this.count(where, params);
1061
- return count > 0;
1062
- }
1063
-
1064
- /**
1065
- * Paginate results
1066
- */
1067
- async paginate(
1068
- page: number,
1069
- limit: number,
1070
- where?: string,
1071
- params: unknown[] = [],
1072
- ): Promise<{
1073
- data: T[];
1074
- total: number;
1075
- page: number;
1076
- limit: number;
1077
- totalPages: number;
1078
- }> {
1079
- const offset = (page - 1) * limit;
1080
-
1081
- const whereClause = where ? `WHERE ${where}` : "";
1082
-
1083
- const [data, countResult] = await Promise.all([
1084
- this.db.raw<T>(
1085
- `SELECT * FROM ${this.tableName} ${whereClause} LIMIT $${params.length + 1} OFFSET $${params.length + 2}`,
1086
- [...params, limit, offset],
1087
- ),
1088
- this.db.raw<{ count: number | string }>(
1089
- `SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`,
1090
- params,
1091
- ),
1092
- ]);
1093
-
1094
- const total = Number(countResult[0]?.count ?? 0);
1095
-
1096
- return {
1097
- data,
1098
- total,
1099
- page,
1100
- limit,
1101
- totalPages: Math.ceil(total / limit),
1102
- };
1103
- }
1104
- }
1105
-
1106
- /**
1107
- * Create a query builder for a table
1108
- */
1109
- export function table<T = unknown>(
1110
- db: Database,
1111
- tableName: string,
1112
- ): QueryBuilder<T> {
1113
- return new QueryBuilder<T>(db, tableName);
1114
- }
1115
-
1116
937
  // ============= SQL Helpers =============
1117
938
 
1118
939
  /**
@@ -1136,7 +957,3 @@ export function buildInClause(values: unknown[]): {
1136
957
  params: values,
1137
958
  };
1138
959
  }
1139
-
1140
- // Re-export schema and migrations
1141
- export * from "./schema";
1142
- export * from "./migrations";
@@ -0,0 +1,457 @@
1
+ /**
2
+ * ORM Query Builder
3
+ *
4
+ * Fluent, chainable query builder for SELECT, INSERT, UPDATE, DELETE operations.
5
+ * Works independently of Model; can be used standalone.
6
+ */
7
+
8
+ import type { Database } from "../index";
9
+ import {
10
+ type CompiledQuery,
11
+ type JoinClause,
12
+ type OrderClause,
13
+ QueryCompiler,
14
+ type QueryState,
15
+ type SqlDialect,
16
+ type WhereClause,
17
+ } from "./compiler";
18
+
19
+ export interface PaginationResult<T> {
20
+ data: T[];
21
+ total: number;
22
+ page: number;
23
+ limit: number;
24
+ totalPages: number;
25
+ }
26
+
27
+ /**
28
+ * Standalone ORM Query Builder
29
+ * Generic over the row type T returned by queries
30
+ */
31
+ export class OrmQueryBuilder<
32
+ T extends Record<string, unknown> = Record<string, unknown>,
33
+ > {
34
+ protected state: QueryState;
35
+ protected compiler: QueryCompiler;
36
+ protected db: Database;
37
+ protected rowTransformer?: (row: Record<string, unknown>) => T;
38
+
39
+ constructor(db: Database, table: string, dialect?: SqlDialect) {
40
+ this.db = db;
41
+ this.compiler = new QueryCompiler(dialect || db.getDriver());
42
+ this.state = {
43
+ table,
44
+ selects: [],
45
+ wheres: [],
46
+ orders: [],
47
+ joins: [],
48
+ groupBys: [],
49
+ havings: [],
50
+ distinct: false,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Clone this builder to a new instance (used by scopes, relationships)
56
+ */
57
+ clone(): OrmQueryBuilder<T> {
58
+ const cloned = new OrmQueryBuilder<T>(this.db, this.state.table);
59
+ cloned.state = {
60
+ ...this.state,
61
+ wheres: [...this.state.wheres],
62
+ orders: [...this.state.orders],
63
+ joins: [...this.state.joins],
64
+ selects: [...this.state.selects],
65
+ groupBys: [...this.state.groupBys],
66
+ havings: [...this.state.havings],
67
+ };
68
+ cloned.compiler = this.compiler;
69
+ cloned.rowTransformer = this.rowTransformer;
70
+ return cloned;
71
+ }
72
+
73
+ /**
74
+ * Set row transformer (called after fetch, before return)
75
+ */
76
+ setRowTransformer(fn: (row: Record<string, unknown>) => T): this {
77
+ this.rowTransformer = fn;
78
+ return this;
79
+ }
80
+
81
+ // ============= SELECT =============
82
+
83
+ select(...columns: string[]): this {
84
+ this.state.selects = columns;
85
+ return this;
86
+ }
87
+
88
+ addSelect(...columns: string[]): this {
89
+ this.state.selects.push(...columns);
90
+ return this;
91
+ }
92
+
93
+ distinct(): this {
94
+ this.state.distinct = true;
95
+ return this;
96
+ }
97
+
98
+ // ============= WHERE =============
99
+
100
+ where(column: string, operatorOrValue: unknown, value?: unknown): this {
101
+ const [operator, val] =
102
+ value === undefined
103
+ ? ["=", operatorOrValue]
104
+ : [String(operatorOrValue), value];
105
+
106
+ this.state.wheres.push({
107
+ type: "and",
108
+ column,
109
+ operator,
110
+ value: val,
111
+ });
112
+ return this;
113
+ }
114
+
115
+ orWhere(column: string, operatorOrValue: unknown, value?: unknown): this {
116
+ const [operator, val] =
117
+ value === undefined
118
+ ? ["=", operatorOrValue]
119
+ : [String(operatorOrValue), value];
120
+
121
+ this.state.wheres.push({
122
+ type: "or",
123
+ column,
124
+ operator,
125
+ value: val,
126
+ });
127
+ return this;
128
+ }
129
+
130
+ whereRaw(sql: string, params?: unknown[]): this {
131
+ this.state.wheres.push({
132
+ type: "and",
133
+ raw: sql,
134
+ rawParams: params,
135
+ });
136
+ return this;
137
+ }
138
+
139
+ whereIn(column: string, values: unknown[]): this {
140
+ this.state.wheres.push({
141
+ type: "and",
142
+ column,
143
+ operator: "IN",
144
+ value: values,
145
+ });
146
+ return this;
147
+ }
148
+
149
+ whereNotIn(column: string, values: unknown[]): this {
150
+ this.state.wheres.push({
151
+ type: "and",
152
+ column,
153
+ operator: "NOT IN",
154
+ value: values,
155
+ });
156
+ return this;
157
+ }
158
+
159
+ whereNull(column: string): this {
160
+ this.state.wheres.push({
161
+ type: "and",
162
+ column,
163
+ operator: "IS NULL",
164
+ });
165
+ return this;
166
+ }
167
+
168
+ whereNotNull(column: string): this {
169
+ this.state.wheres.push({
170
+ type: "and",
171
+ column,
172
+ operator: "IS NOT NULL",
173
+ });
174
+ return this;
175
+ }
176
+
177
+ whereBetween(column: string, min: unknown, max: unknown): this {
178
+ this.state.wheres.push({
179
+ type: "and",
180
+ column,
181
+ operator: "BETWEEN",
182
+ value: [min, max],
183
+ });
184
+ return this;
185
+ }
186
+
187
+ // ============= JOIN =============
188
+
189
+ join(
190
+ table: string,
191
+ on: string,
192
+ type: "INNER" | "LEFT" | "RIGHT" = "INNER",
193
+ ): this {
194
+ this.state.joins.push({ type, table, on });
195
+ return this;
196
+ }
197
+
198
+ leftJoin(table: string, on: string): this {
199
+ return this.join(table, on, "LEFT");
200
+ }
201
+
202
+ rightJoin(table: string, on: string): this {
203
+ return this.join(table, on, "RIGHT");
204
+ }
205
+
206
+ crossJoin(table: string): this {
207
+ this.state.joins.push({ type: "CROSS", table, on: "" });
208
+ return this;
209
+ }
210
+
211
+ // ============= GROUP BY / HAVING =============
212
+
213
+ groupBy(...columns: string[]): this {
214
+ this.state.groupBys.push(...columns);
215
+ return this;
216
+ }
217
+
218
+ having(raw: string, params?: unknown[]): this {
219
+ this.state.havings.push(raw);
220
+ if (params) {
221
+ // HAVING with params is more complex; for now, just support raw SQL
222
+ }
223
+ return this;
224
+ }
225
+
226
+ // ============= ORDER BY =============
227
+
228
+ orderBy(column: string, direction: "ASC" | "DESC" = "ASC"): this {
229
+ this.state.orders.push({ column, direction });
230
+ return this;
231
+ }
232
+
233
+ orderByDesc(column: string): this {
234
+ return this.orderBy(column, "DESC");
235
+ }
236
+
237
+ // ============= LIMIT / OFFSET =============
238
+
239
+ limit(n: number): this {
240
+ this.state.limitVal = n;
241
+ return this;
242
+ }
243
+
244
+ offset(n: number): this {
245
+ this.state.offsetVal = n;
246
+ return this;
247
+ }
248
+
249
+ // ============= LOCKING =============
250
+
251
+ lockForShare(): this {
252
+ this.state.lockMode = "share";
253
+ return this;
254
+ }
255
+
256
+ lockForUpdate(): this {
257
+ this.state.lockMode = "update";
258
+ return this;
259
+ }
260
+
261
+ // ============= TERMINAL METHODS =============
262
+
263
+ /**
264
+ * Fetch all rows
265
+ */
266
+ async get(): Promise<T[]> {
267
+ const { sql, params } = this.compiler.compileSelect(this.state);
268
+ const rows = await this.db.raw<Record<string, unknown>>(sql, params);
269
+ return rows.map((row) =>
270
+ this.rowTransformer ? this.rowTransformer(row) : (row as T),
271
+ );
272
+ }
273
+
274
+ /**
275
+ * Fetch first row
276
+ */
277
+ async first(): Promise<T | null> {
278
+ const results = await this.limit(1).get();
279
+ return results.length > 0 ? results[0] : null;
280
+ }
281
+
282
+ /**
283
+ * Fetch first row or throw
284
+ */
285
+ async firstOrFail(): Promise<T> {
286
+ const result = await this.first();
287
+ if (!result) {
288
+ throw new Error(`No record found for query`);
289
+ }
290
+ return result;
291
+ }
292
+
293
+ /**
294
+ * Find by primary key (assumes 'id' column)
295
+ */
296
+ async find(id: unknown): Promise<T | null> {
297
+ return this.where("id", id).first();
298
+ }
299
+
300
+ /**
301
+ * Find by primary key or throw
302
+ */
303
+ async findOrFail(id: unknown): Promise<T> {
304
+ const result = await this.find(id);
305
+ if (!result) {
306
+ throw new Error(`Record with id ${id} not found`);
307
+ }
308
+ return result;
309
+ }
310
+
311
+ /**
312
+ * Count rows
313
+ */
314
+ async count(column = "*"): Promise<number> {
315
+ const { sql, params } = this.compiler.compileCount(this.state, column);
316
+ const rows = await this.db.raw<{ count: string | number }>(sql, params);
317
+ return Number(rows[0]?.count ?? 0);
318
+ }
319
+
320
+ /**
321
+ * Check if any rows exist
322
+ */
323
+ async exists(): Promise<boolean> {
324
+ const count = await this.count();
325
+ return count > 0;
326
+ }
327
+
328
+ /**
329
+ * Pluck a single column
330
+ */
331
+ async pluck<K extends keyof T>(column: K): Promise<T[K][]> {
332
+ const results = await this.select(String(column)).get();
333
+ return results.map((row) => row[column]);
334
+ }
335
+
336
+ /**
337
+ * Get a single column value
338
+ */
339
+ async value<K extends keyof T>(column: K): Promise<T[K] | null> {
340
+ const result = await this.select(String(column)).first();
341
+ return result ? result[column] : null;
342
+ }
343
+
344
+ /**
345
+ * Paginate results
346
+ */
347
+ async paginate(page: number, limit: number): Promise<PaginationResult<T>> {
348
+ const offset = (page - 1) * limit;
349
+ const [data, total] = await Promise.all([
350
+ this.clone().offset(offset).limit(limit).get(),
351
+ this.clone().count(),
352
+ ]);
353
+
354
+ return {
355
+ data,
356
+ total,
357
+ page,
358
+ limit,
359
+ totalPages: Math.ceil(total / limit),
360
+ };
361
+ }
362
+
363
+ /**
364
+ * Insert a single row
365
+ */
366
+ async insert(data: Partial<T>): Promise<T> {
367
+ const { sql, params } = this.compiler.compileInsert(
368
+ this.state.table,
369
+ data as Record<string, unknown>,
370
+ );
371
+
372
+ if (this.db.getDriver() === "postgresql") {
373
+ const rows = await this.db.raw<Record<string, unknown>>(sql, params);
374
+ return (
375
+ this.rowTransformer ? this.rowTransformer(rows[0]) : rows[0]
376
+ ) as T;
377
+ }
378
+
379
+ // SQLite / MySQL
380
+ await this.db.raw(sql, params);
381
+ const lastId = await this.getLastInsertId();
382
+ const row = await this.db.raw<Record<string, unknown>>(
383
+ `SELECT * FROM ${this.state.table} WHERE id = ?`,
384
+ [lastId],
385
+ );
386
+ return (this.rowTransformer ? this.rowTransformer(row[0]) : row[0]) as T;
387
+ }
388
+
389
+ /**
390
+ * Insert multiple rows
391
+ */
392
+ async insertMany(items: Partial<T>[]): Promise<T[]> {
393
+ const results: T[] = [];
394
+ for (const item of items) {
395
+ const result = await this.insert(item);
396
+ results.push(result);
397
+ }
398
+ return results;
399
+ }
400
+
401
+ /**
402
+ * Update rows matching the query
403
+ */
404
+ async update(data: Partial<T>): Promise<number> {
405
+ const { sql, params } = this.compiler.compileUpdate(
406
+ this.state,
407
+ data as Record<string, unknown>,
408
+ );
409
+
410
+ if (this.db.getDriver() === "postgresql") {
411
+ const rows = await this.db.raw<Record<string, unknown>>(sql, params);
412
+ return rows.length;
413
+ }
414
+
415
+ // SQLite / MySQL don't have RETURNING, use changes() if available
416
+ await this.db.raw(sql, params);
417
+ return 0; // TODO: get affected row count from Bun.SQL
418
+ }
419
+
420
+ /**
421
+ * Delete rows matching the query
422
+ */
423
+ async delete(): Promise<number> {
424
+ const { sql, params } = this.compiler.compileDelete(this.state);
425
+ await this.db.raw(sql, params);
426
+ return 0; // TODO: get affected row count from Bun.SQL
427
+ }
428
+
429
+ /**
430
+ * Get last insert ID (SQLite/MySQL only)
431
+ */
432
+ private async getLastInsertId(): Promise<number | string> {
433
+ const driver = this.db.getDriver();
434
+ if (driver === "sqlite") {
435
+ const row = await this.db.raw<{ id: number }>(
436
+ "SELECT last_insert_rowid() as id",
437
+ );
438
+ return row[0].id;
439
+ }
440
+ if (driver === "mysql") {
441
+ const row = await this.db.raw<{ id: number }>(
442
+ "SELECT LAST_INSERT_ID() as id",
443
+ );
444
+ return row[0].id;
445
+ }
446
+ throw new Error("Unexpected driver");
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Factory function to create a query builder
452
+ */
453
+ export function query<
454
+ T extends Record<string, unknown> = Record<string, unknown>,
455
+ >(db: Database, table: string): OrmQueryBuilder<T> {
456
+ return new OrmQueryBuilder<T>(db, table);
457
+ }