@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.
- package/README.md +264 -17
- package/dist/cli/{index.js → bin.js} +413 -332
- package/dist/container/index.js +273 -0
- package/dist/context/index.js +219 -0
- package/dist/database/index.js +493 -0
- package/dist/frontend/index.js +7697 -0
- package/dist/graphql/index.js +2156 -0
- package/dist/health/index.js +364 -0
- package/dist/i18n/index.js +345 -0
- package/dist/index.js +9694 -5047
- package/dist/jobs/index.js +819 -0
- package/dist/lock/index.js +367 -0
- package/dist/logger/index.js +281 -0
- package/dist/metrics/index.js +289 -0
- package/dist/middleware/index.js +77 -0
- package/dist/migrations/index.js +571 -0
- package/dist/modules/index.js +3411 -0
- package/dist/notification/index.js +484 -0
- package/dist/observability/index.js +331 -0
- package/dist/openapi/index.js +795 -0
- package/dist/orm/index.js +1356 -0
- package/dist/router/index.js +886 -0
- package/dist/rpc/index.js +691 -0
- package/dist/schema/index.js +400 -0
- package/dist/telemetry/index.js +595 -0
- package/dist/template/index.js +640 -0
- package/dist/templates/index.js +640 -0
- package/dist/testing/index.js +1111 -0
- package/dist/types/index.js +60 -0
- package/llms.txt +231 -0
- package/package.json +125 -27
- package/src/cache/index.ts +2 -1
- package/src/cli/ARCHITECTURE.md +3 -3
- package/src/cli/bin.ts +2 -2
- package/src/cli/commands/build.ts +183 -165
- package/src/cli/commands/dev.ts +96 -89
- package/src/cli/commands/generate.ts +142 -111
- package/src/cli/commands/help.ts +20 -16
- package/src/cli/commands/index.ts +3 -6
- package/src/cli/commands/migration.ts +124 -105
- package/src/cli/commands/new.ts +294 -232
- package/src/cli/commands/start.ts +81 -79
- package/src/cli/core/args.ts +68 -50
- package/src/cli/core/console.ts +89 -95
- package/src/cli/core/index.ts +4 -4
- package/src/cli/core/prompt.ts +65 -62
- package/src/cli/core/spinner.ts +23 -20
- package/src/cli/index.ts +46 -38
- package/src/cli/templates/database/index.ts +37 -18
- package/src/cli/templates/database/mysql.ts +3 -3
- package/src/cli/templates/database/none.ts +2 -2
- package/src/cli/templates/database/postgresql.ts +3 -3
- package/src/cli/templates/database/sqlite.ts +3 -3
- package/src/cli/templates/deploy.ts +29 -26
- package/src/cli/templates/docker.ts +41 -30
- package/src/cli/templates/frontend/index.ts +33 -15
- package/src/cli/templates/frontend/none.ts +2 -2
- package/src/cli/templates/frontend/react.ts +18 -18
- package/src/cli/templates/frontend/solid.ts +15 -15
- package/src/cli/templates/frontend/svelte.ts +17 -17
- package/src/cli/templates/frontend/vue.ts +15 -15
- package/src/cli/templates/generators/index.ts +29 -29
- package/src/cli/templates/generators/types.ts +21 -21
- package/src/cli/templates/index.ts +6 -6
- package/src/cli/templates/project/api.ts +37 -36
- package/src/cli/templates/project/default.ts +25 -25
- package/src/cli/templates/project/fullstack.ts +28 -26
- package/src/cli/templates/project/index.ts +55 -16
- package/src/cli/templates/project/minimal.ts +17 -12
- package/src/cli/templates/project/types.ts +10 -5
- package/src/cli/templates/project/website.ts +15 -15
- package/src/cli/utils/fs.ts +55 -41
- package/src/cli/utils/index.ts +3 -3
- package/src/cli/utils/strings.ts +47 -33
- package/src/cli/utils/version.ts +14 -8
- package/src/config/env-validation.ts +100 -0
- package/src/config/env.ts +169 -41
- package/src/config/index.ts +28 -20
- package/src/config/loader.ts +25 -16
- package/src/config/merge.ts +21 -10
- package/src/config/types.ts +566 -25
- package/src/config/validation.ts +215 -7
- package/src/container/forward-ref.ts +22 -22
- package/src/container/index.ts +34 -12
- package/src/context/index.ts +11 -1
- package/src/database/index.ts +7 -190
- package/src/database/orm/builder.ts +457 -0
- package/src/database/orm/casts/index.ts +130 -0
- package/src/database/orm/casts/types.ts +25 -0
- package/src/database/orm/compiler.ts +304 -0
- package/src/database/orm/hooks/index.ts +114 -0
- package/src/database/orm/index.ts +61 -0
- package/src/database/orm/model-registry.ts +59 -0
- package/src/database/orm/model.ts +821 -0
- package/src/database/orm/relationships/base.ts +146 -0
- package/src/database/orm/relationships/belongs-to-many.ts +179 -0
- package/src/database/orm/relationships/belongs-to.ts +56 -0
- package/src/database/orm/relationships/has-many.ts +45 -0
- package/src/database/orm/relationships/has-one.ts +41 -0
- package/src/database/orm/relationships/index.ts +11 -0
- package/src/database/orm/scopes/index.ts +55 -0
- package/src/events/__tests__/event-system.test.ts +235 -0
- package/src/events/config.ts +238 -0
- package/src/events/example-usage.ts +185 -0
- package/src/events/index.ts +278 -0
- package/src/events/manager.ts +385 -0
- package/src/events/registry.ts +182 -0
- package/src/events/types.ts +124 -0
- package/src/frontend/api-routes.ts +65 -23
- package/src/frontend/bundler.ts +76 -34
- package/src/frontend/console-client.ts +2 -2
- package/src/frontend/console-stream.ts +94 -38
- package/src/frontend/dev-server.ts +94 -46
- package/src/frontend/file-router.ts +61 -19
- package/src/frontend/frameworks/index.ts +37 -10
- package/src/frontend/frameworks/react.ts +10 -8
- package/src/frontend/frameworks/solid.ts +11 -9
- package/src/frontend/frameworks/svelte.ts +15 -9
- package/src/frontend/frameworks/vue.ts +13 -11
- package/src/frontend/hmr-client.ts +12 -10
- package/src/frontend/hmr.ts +146 -103
- package/src/frontend/index.ts +14 -5
- package/src/frontend/islands.ts +41 -22
- package/src/frontend/isr.ts +59 -37
- package/src/frontend/layout.ts +36 -21
- package/src/frontend/ssr/react.ts +74 -27
- package/src/frontend/ssr/solid.ts +54 -20
- package/src/frontend/ssr/svelte.ts +48 -14
- package/src/frontend/ssr/vue.ts +50 -18
- package/src/frontend/ssr.ts +83 -39
- package/src/frontend/types.ts +91 -56
- package/src/graphql/built-in-engine.ts +598 -0
- package/src/graphql/context-builder.ts +110 -0
- package/src/graphql/decorators.ts +358 -0
- package/src/graphql/execution-pipeline.ts +227 -0
- package/src/graphql/graphql-module.ts +563 -0
- package/src/graphql/index.ts +101 -0
- package/src/graphql/metadata.ts +237 -0
- package/src/graphql/schema-builder.ts +319 -0
- package/src/graphql/subscription-handler.ts +283 -0
- package/src/graphql/types.ts +324 -0
- package/src/health/index.ts +21 -9
- package/src/i18n/engine.ts +305 -0
- package/src/i18n/index.ts +38 -0
- package/src/i18n/loader.ts +218 -0
- package/src/i18n/middleware.ts +164 -0
- package/src/i18n/negotiator.ts +162 -0
- package/src/i18n/types.ts +158 -0
- package/src/index.ts +182 -27
- package/src/jobs/drivers/memory.ts +315 -0
- package/src/jobs/drivers/redis.ts +459 -0
- package/src/jobs/index.ts +30 -0
- package/src/jobs/queue.ts +281 -0
- package/src/jobs/types.ts +295 -0
- package/src/jobs/worker.ts +380 -0
- package/src/logger/index.ts +1 -3
- package/src/logger/transports/index.ts +62 -22
- package/src/metrics/index.ts +25 -16
- package/src/migrations/index.ts +9 -0
- package/src/modules/filters.ts +13 -17
- package/src/modules/guards.ts +49 -26
- package/src/modules/index.ts +457 -299
- package/src/modules/interceptors.ts +58 -20
- package/src/modules/lazy.ts +11 -19
- package/src/modules/lifecycle.ts +15 -7
- package/src/modules/metadata.ts +15 -5
- package/src/modules/pipes.ts +94 -72
- package/src/notification/channels/base.ts +68 -0
- package/src/notification/channels/email.ts +105 -0
- package/src/notification/channels/push.ts +104 -0
- package/src/notification/channels/sms.ts +105 -0
- package/src/notification/channels/whatsapp.ts +104 -0
- package/src/notification/index.ts +48 -0
- package/src/notification/service.ts +354 -0
- package/src/notification/types.ts +344 -0
- package/src/observability/__tests__/observability.test.ts +483 -0
- package/src/observability/breadcrumbs.ts +114 -0
- package/src/observability/index.ts +136 -0
- package/src/observability/interceptor.ts +85 -0
- package/src/observability/service.ts +303 -0
- package/src/observability/trace.ts +37 -0
- package/src/observability/types.ts +196 -0
- package/src/openapi/__tests__/decorators.test.ts +335 -0
- package/src/openapi/__tests__/document-builder.test.ts +285 -0
- package/src/openapi/__tests__/route-scanner.test.ts +334 -0
- package/src/openapi/__tests__/schema-generator.test.ts +275 -0
- package/src/openapi/decorators.ts +328 -0
- package/src/openapi/document-builder.ts +274 -0
- package/src/openapi/index.ts +112 -0
- package/src/openapi/metadata.ts +112 -0
- package/src/openapi/route-scanner.ts +289 -0
- package/src/openapi/schema-generator.ts +256 -0
- package/src/openapi/swagger-module.ts +166 -0
- package/src/openapi/types.ts +398 -0
- package/src/orm/index.ts +10 -0
- package/src/rpc/index.ts +3 -1
- package/src/schema/index.ts +9 -0
- package/src/security/index.ts +15 -6
- package/src/ssg/index.ts +9 -8
- package/src/telemetry/index.ts +76 -22
- package/src/template/index.ts +7 -0
- package/src/templates/engine.ts +224 -0
- package/src/templates/index.ts +9 -0
- package/src/templates/loader.ts +331 -0
- package/src/templates/renderers/markdown.ts +212 -0
- package/src/templates/renderers/simple.ts +269 -0
- package/src/templates/types.ts +154 -0
- package/src/testing/index.ts +100 -27
- package/src/types/optional-deps.d.ts +347 -187
- package/src/validation/index.ts +92 -2
- package/src/validation/schemas.ts +536 -0
- package/tests/integration/cli.test.ts +19 -19
- package/tests/integration/fullstack.test.ts +4 -4
- package/tests/unit/cli.test.ts +1 -1
- package/tests/unit/database.test.ts +2 -72
- package/tests/unit/env-validation.test.ts +166 -0
- package/tests/unit/events.test.ts +910 -0
- package/tests/unit/graphql.test.ts +991 -0
- package/tests/unit/i18n.test.ts +455 -0
- package/tests/unit/jobs.test.ts +493 -0
- package/tests/unit/notification.test.ts +988 -0
- package/tests/unit/observability.test.ts +453 -0
- package/tests/unit/orm/builder.test.ts +323 -0
- package/tests/unit/orm/casts.test.ts +179 -0
- package/tests/unit/orm/compiler.test.ts +220 -0
- package/tests/unit/orm/eager-loading.test.ts +285 -0
- package/tests/unit/orm/hooks.test.ts +191 -0
- package/tests/unit/orm/model.test.ts +373 -0
- package/tests/unit/orm/relationships.test.ts +303 -0
- package/tests/unit/orm/scopes.test.ts +74 -0
- package/tests/unit/templates-simple.test.ts +53 -0
- package/tests/unit/templates.test.ts +454 -0
- package/tests/unit/validation.test.ts +18 -24
- package/tsconfig.json +11 -3
package/src/database/index.ts
CHANGED
|
@@ -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(
|
|
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>> =
|
|
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 =
|
|
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
|
+
}
|