@buenojs/bueno 0.8.4 → 0.8.5
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 +136 -16
- package/dist/cli/{index.js → bin.js} +412 -331
- package/dist/container/index.js +250 -0
- package/dist/context/index.js +219 -0
- package/dist/database/index.js +493 -0
- package/dist/frontend/index.js +7697 -0
- package/dist/health/index.js +364 -0
- package/dist/i18n/index.js +345 -0
- package/dist/index.js +11043 -6482
- 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 +3346 -0
- package/dist/notification/index.js +484 -0
- package/dist/observability/index.js +331 -0
- package/dist/openapi/index.js +776 -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/package.json +121 -27
- package/src/cache/index.ts +2 -1
- 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 +14 -14
- 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 +545 -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/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 +179 -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 +409 -298
- 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/fullstack.test.ts +4 -4
- 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/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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cast Registry and Built-in Casts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CastDefinition, CastObject } from "./types";
|
|
6
|
+
|
|
7
|
+
export type { CastDefinition, CastObject, BuiltInCastName } from "./types";
|
|
8
|
+
|
|
9
|
+
const builtInCasts: Record<string, CastObject> = {
|
|
10
|
+
json: {
|
|
11
|
+
get(value: unknown): unknown {
|
|
12
|
+
if (typeof value === "string") {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(value);
|
|
15
|
+
} catch {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
},
|
|
21
|
+
set(value: unknown): unknown {
|
|
22
|
+
if (typeof value === "string") return value;
|
|
23
|
+
return JSON.stringify(value);
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
boolean: {
|
|
28
|
+
get(value: unknown): boolean | null {
|
|
29
|
+
if (value === null || value === undefined) return null;
|
|
30
|
+
if (typeof value === "boolean") return value;
|
|
31
|
+
if (typeof value === "number") return value !== 0;
|
|
32
|
+
if (typeof value === "string") return value !== "0" && value !== "false";
|
|
33
|
+
return Boolean(value);
|
|
34
|
+
},
|
|
35
|
+
set(value: unknown): number {
|
|
36
|
+
return value ? 1 : 0;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
integer: {
|
|
41
|
+
get(value: unknown): number {
|
|
42
|
+
return Number(value);
|
|
43
|
+
},
|
|
44
|
+
set(value: unknown): number {
|
|
45
|
+
return Number(value);
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
float: {
|
|
50
|
+
get(value: unknown): number {
|
|
51
|
+
return Number(value);
|
|
52
|
+
},
|
|
53
|
+
set(value: unknown): number {
|
|
54
|
+
return Number(value);
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
date: {
|
|
59
|
+
get(value: unknown): Date | null {
|
|
60
|
+
if (!value) return null;
|
|
61
|
+
if (value instanceof Date) return value;
|
|
62
|
+
return new Date(String(value));
|
|
63
|
+
},
|
|
64
|
+
set(value: unknown): string {
|
|
65
|
+
if (value instanceof Date) {
|
|
66
|
+
return value.toISOString().split("T")[0];
|
|
67
|
+
}
|
|
68
|
+
return String(value);
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
datetime: {
|
|
73
|
+
get(value: unknown): Date | null {
|
|
74
|
+
if (!value) return null;
|
|
75
|
+
if (value instanceof Date) return value;
|
|
76
|
+
return new Date(String(value));
|
|
77
|
+
},
|
|
78
|
+
set(value: unknown): string {
|
|
79
|
+
if (value instanceof Date) {
|
|
80
|
+
return value.toISOString();
|
|
81
|
+
}
|
|
82
|
+
return String(value);
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
timestamp: {
|
|
87
|
+
get(value: unknown): Date | null {
|
|
88
|
+
if (!value) return null;
|
|
89
|
+
if (value instanceof Date) return value;
|
|
90
|
+
if (typeof value === "number") return new Date(value);
|
|
91
|
+
return new Date(Number(value));
|
|
92
|
+
},
|
|
93
|
+
set(value: unknown): number {
|
|
94
|
+
if (value instanceof Date) {
|
|
95
|
+
return value.getTime();
|
|
96
|
+
}
|
|
97
|
+
return Number(value);
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export class CastRegistry {
|
|
103
|
+
/**
|
|
104
|
+
* Deserialize a value from database using a cast definition
|
|
105
|
+
*/
|
|
106
|
+
static deserialize(castDef: CastDefinition, value: unknown): unknown {
|
|
107
|
+
if (typeof castDef === "string") {
|
|
108
|
+
const castObj = builtInCasts[castDef];
|
|
109
|
+
if (!castObj) {
|
|
110
|
+
throw new Error(`Unknown cast: ${castDef}`);
|
|
111
|
+
}
|
|
112
|
+
return castObj.get(value);
|
|
113
|
+
}
|
|
114
|
+
return castDef.get(value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Serialize a value to database using a cast definition
|
|
119
|
+
*/
|
|
120
|
+
static serialize(castDef: CastDefinition, value: unknown): unknown {
|
|
121
|
+
if (typeof castDef === "string") {
|
|
122
|
+
const castObj = builtInCasts[castDef];
|
|
123
|
+
if (!castObj) {
|
|
124
|
+
throw new Error(`Unknown cast: ${castDef}`);
|
|
125
|
+
}
|
|
126
|
+
return castObj.set(value);
|
|
127
|
+
}
|
|
128
|
+
return castDef.set(value);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cast Type Definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type BuiltInCastName =
|
|
6
|
+
| "json"
|
|
7
|
+
| "boolean"
|
|
8
|
+
| "integer"
|
|
9
|
+
| "float"
|
|
10
|
+
| "date"
|
|
11
|
+
| "datetime"
|
|
12
|
+
| "timestamp";
|
|
13
|
+
|
|
14
|
+
export interface CastObject {
|
|
15
|
+
/**
|
|
16
|
+
* Transform value from database to model attribute
|
|
17
|
+
*/
|
|
18
|
+
get(value: unknown): unknown;
|
|
19
|
+
/**
|
|
20
|
+
* Transform value from model attribute to database
|
|
21
|
+
*/
|
|
22
|
+
set(value: unknown): unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type CastDefinition = BuiltInCastName | CastObject;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL Query Compiler
|
|
3
|
+
*
|
|
4
|
+
* Converts a QueryState into SQL strings and parameters,
|
|
5
|
+
* handling driver differences (PostgreSQL $1 vs SQLite/MySQL ?)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type SqlDialect = "postgresql" | "mysql" | "sqlite";
|
|
9
|
+
|
|
10
|
+
export interface CompiledQuery {
|
|
11
|
+
sql: string;
|
|
12
|
+
params: unknown[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface WhereClause {
|
|
16
|
+
type: "and" | "or";
|
|
17
|
+
column?: string;
|
|
18
|
+
operator?: string;
|
|
19
|
+
value?: unknown;
|
|
20
|
+
raw?: string;
|
|
21
|
+
rawParams?: unknown[];
|
|
22
|
+
nested?: WhereClause[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface OrderClause {
|
|
26
|
+
column: string;
|
|
27
|
+
direction: "ASC" | "DESC";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface JoinClause {
|
|
31
|
+
type: "INNER" | "LEFT" | "RIGHT" | "CROSS";
|
|
32
|
+
table: string;
|
|
33
|
+
on: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface QueryState {
|
|
37
|
+
table: string;
|
|
38
|
+
alias?: string;
|
|
39
|
+
selects: string[];
|
|
40
|
+
wheres: WhereClause[];
|
|
41
|
+
orders: OrderClause[];
|
|
42
|
+
joins: JoinClause[];
|
|
43
|
+
limitVal?: number;
|
|
44
|
+
offsetVal?: number;
|
|
45
|
+
groupBys: string[];
|
|
46
|
+
havings: string[];
|
|
47
|
+
distinct: boolean;
|
|
48
|
+
lockMode?: "share" | "update";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class QueryCompiler {
|
|
52
|
+
private paramIndex = 0;
|
|
53
|
+
private params: unknown[] = [];
|
|
54
|
+
|
|
55
|
+
constructor(private dialect: SqlDialect) {}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Reset state for a new compilation
|
|
59
|
+
*/
|
|
60
|
+
private reset(): void {
|
|
61
|
+
this.paramIndex = 0;
|
|
62
|
+
this.params = [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Add a parameter and return the placeholder for this dialect
|
|
67
|
+
*/
|
|
68
|
+
private addParam(value: unknown): string {
|
|
69
|
+
this.params.push(value);
|
|
70
|
+
if (this.dialect === "postgresql") {
|
|
71
|
+
return `$${++this.paramIndex}`;
|
|
72
|
+
}
|
|
73
|
+
this.paramIndex++;
|
|
74
|
+
return "?";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Compile a SELECT query
|
|
79
|
+
*/
|
|
80
|
+
compileSelect(state: QueryState): CompiledQuery {
|
|
81
|
+
this.reset();
|
|
82
|
+
const parts: string[] = [];
|
|
83
|
+
|
|
84
|
+
// SELECT
|
|
85
|
+
const cols = state.selects.length > 0 ? state.selects.join(", ") : "*";
|
|
86
|
+
const distinct = state.distinct ? "DISTINCT " : "";
|
|
87
|
+
parts.push(`SELECT ${distinct}${cols}`);
|
|
88
|
+
|
|
89
|
+
// FROM
|
|
90
|
+
const alias = state.alias ? ` AS ${state.alias}` : "";
|
|
91
|
+
parts.push(`FROM ${state.table}${alias}`);
|
|
92
|
+
|
|
93
|
+
// JOINs
|
|
94
|
+
for (const j of state.joins) {
|
|
95
|
+
parts.push(`${j.type} JOIN ${j.table} ON ${j.on}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// WHERE
|
|
99
|
+
if (state.wheres.length > 0) {
|
|
100
|
+
parts.push(`WHERE ${this.compileWheres(state.wheres)}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// GROUP BY
|
|
104
|
+
if (state.groupBys.length > 0) {
|
|
105
|
+
parts.push(`GROUP BY ${state.groupBys.join(", ")}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// HAVING
|
|
109
|
+
if (state.havings.length > 0) {
|
|
110
|
+
parts.push(`HAVING ${state.havings.join(" AND ")}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ORDER BY
|
|
114
|
+
if (state.orders.length > 0) {
|
|
115
|
+
const orderStr = state.orders
|
|
116
|
+
.map((o) => `${o.column} ${o.direction}`)
|
|
117
|
+
.join(", ");
|
|
118
|
+
parts.push(`ORDER BY ${orderStr}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// LIMIT / OFFSET
|
|
122
|
+
if (state.limitVal !== undefined) {
|
|
123
|
+
parts.push(`LIMIT ${this.addParam(state.limitVal)}`);
|
|
124
|
+
}
|
|
125
|
+
if (state.offsetVal !== undefined) {
|
|
126
|
+
parts.push(`OFFSET ${this.addParam(state.offsetVal)}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Locking
|
|
130
|
+
if (state.lockMode === "update") parts.push("FOR UPDATE");
|
|
131
|
+
if (state.lockMode === "share") parts.push("FOR SHARE");
|
|
132
|
+
|
|
133
|
+
return { sql: parts.join(" "), params: [...this.params] };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Compile WHERE clauses into SQL
|
|
138
|
+
*/
|
|
139
|
+
private compileWheres(wheres: WhereClause[]): string {
|
|
140
|
+
return wheres
|
|
141
|
+
.map((w, i) => {
|
|
142
|
+
const prefix = i === 0 ? "" : `${w.type.toUpperCase()} `;
|
|
143
|
+
|
|
144
|
+
if (w.raw !== undefined) {
|
|
145
|
+
if (w.rawParams) {
|
|
146
|
+
for (const p of w.rawParams) {
|
|
147
|
+
this.params.push(p);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return `${prefix}(${w.raw})`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (w.nested) {
|
|
154
|
+
return `${prefix}(${this.compileWheres(w.nested)})`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (w.operator === "IN" && Array.isArray(w.value)) {
|
|
158
|
+
const placeholders = w.value.map((v) => this.addParam(v)).join(", ");
|
|
159
|
+
return `${prefix}${w.column} IN (${placeholders})`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (w.operator === "NOT IN" && Array.isArray(w.value)) {
|
|
163
|
+
const placeholders = w.value.map((v) => this.addParam(v)).join(", ");
|
|
164
|
+
return `${prefix}${w.column} NOT IN (${placeholders})`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (w.operator === "IS NULL") {
|
|
168
|
+
return `${prefix}${w.column} IS NULL`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (w.operator === "IS NOT NULL") {
|
|
172
|
+
return `${prefix}${w.column} IS NOT NULL`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (w.operator === "BETWEEN") {
|
|
176
|
+
const [min, max] = w.value as [unknown, unknown];
|
|
177
|
+
return `${prefix}${w.column} BETWEEN ${this.addParam(min)} AND ${this.addParam(max)}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return `${prefix}${w.column} ${w.operator} ${this.addParam(w.value)}`;
|
|
181
|
+
})
|
|
182
|
+
.join(" ");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Compile an INSERT query
|
|
187
|
+
*/
|
|
188
|
+
compileInsert(table: string, data: Record<string, unknown>): CompiledQuery {
|
|
189
|
+
this.reset();
|
|
190
|
+
const keys = Object.keys(data);
|
|
191
|
+
const columns = keys.join(", ");
|
|
192
|
+
const placeholders = keys.map((k) => this.addParam(data[k])).join(", ");
|
|
193
|
+
|
|
194
|
+
const returning = this.dialect === "postgresql" ? " RETURNING *" : "";
|
|
195
|
+
return {
|
|
196
|
+
sql: `INSERT INTO ${table} (${columns}) VALUES (${placeholders})${returning}`,
|
|
197
|
+
params: [...this.params],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Compile a batch INSERT query
|
|
203
|
+
*/
|
|
204
|
+
compileBatchInsert(
|
|
205
|
+
table: string,
|
|
206
|
+
rows: Record<string, unknown>[],
|
|
207
|
+
): CompiledQuery {
|
|
208
|
+
this.reset();
|
|
209
|
+
|
|
210
|
+
if (rows.length === 0) {
|
|
211
|
+
return { sql: "", params: [] };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const keys = Object.keys(rows[0]);
|
|
215
|
+
const columns = keys.join(", ");
|
|
216
|
+
|
|
217
|
+
const valueRows = rows.map((row) => {
|
|
218
|
+
const placeholders = keys.map((k) => this.addParam(row[k])).join(", ");
|
|
219
|
+
return `(${placeholders})`;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const returning = this.dialect === "postgresql" ? " RETURNING *" : "";
|
|
223
|
+
return {
|
|
224
|
+
sql: `INSERT INTO ${table} (${columns}) VALUES ${valueRows.join(", ")}${returning}`,
|
|
225
|
+
params: [...this.params],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Compile an UPDATE query
|
|
231
|
+
*/
|
|
232
|
+
compileUpdate(
|
|
233
|
+
state: QueryState,
|
|
234
|
+
data: Record<string, unknown>,
|
|
235
|
+
): CompiledQuery {
|
|
236
|
+
this.reset();
|
|
237
|
+
const sets = Object.entries(data)
|
|
238
|
+
.map(([col, val]) => `${col} = ${this.addParam(val)}`)
|
|
239
|
+
.join(", ");
|
|
240
|
+
|
|
241
|
+
const whereStr =
|
|
242
|
+
state.wheres.length > 0
|
|
243
|
+
? ` WHERE ${this.compileWheres(state.wheres)}`
|
|
244
|
+
: "";
|
|
245
|
+
|
|
246
|
+
const returning = this.dialect === "postgresql" ? " RETURNING *" : "";
|
|
247
|
+
return {
|
|
248
|
+
sql: `UPDATE ${state.table} SET ${sets}${whereStr}${returning}`,
|
|
249
|
+
params: [...this.params],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Compile a DELETE query
|
|
255
|
+
*/
|
|
256
|
+
compileDelete(state: QueryState): CompiledQuery {
|
|
257
|
+
this.reset();
|
|
258
|
+
const whereStr =
|
|
259
|
+
state.wheres.length > 0
|
|
260
|
+
? ` WHERE ${this.compileWheres(state.wheres)}`
|
|
261
|
+
: "";
|
|
262
|
+
return {
|
|
263
|
+
sql: `DELETE FROM ${state.table}${whereStr}`,
|
|
264
|
+
params: [...this.params],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Compile a COUNT query
|
|
270
|
+
*/
|
|
271
|
+
compileCount(state: QueryState, column = "*"): CompiledQuery {
|
|
272
|
+
this.reset();
|
|
273
|
+
const countCol = column === "*" ? "COUNT(*)" : `COUNT(${column})`;
|
|
274
|
+
const parts: string[] = [`SELECT ${countCol} AS count`];
|
|
275
|
+
|
|
276
|
+
const alias = state.alias ? ` AS ${state.alias}` : "";
|
|
277
|
+
parts.push(`FROM ${state.table}${alias}`);
|
|
278
|
+
|
|
279
|
+
for (const j of state.joins) {
|
|
280
|
+
parts.push(`${j.type} JOIN ${j.table} ON ${j.on}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (state.wheres.length > 0) {
|
|
284
|
+
parts.push(`WHERE ${this.compileWheres(state.wheres)}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return { sql: parts.join(" "), params: [...this.params] };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Compile an EXISTS query
|
|
292
|
+
*/
|
|
293
|
+
compileExists(state: QueryState): CompiledQuery {
|
|
294
|
+
const selectCompiled = this.compileSelect({
|
|
295
|
+
...state,
|
|
296
|
+
selects: ["1"],
|
|
297
|
+
limitVal: 1,
|
|
298
|
+
});
|
|
299
|
+
return {
|
|
300
|
+
sql: `SELECT EXISTS(${selectCompiled.sql}) as exists`,
|
|
301
|
+
params: selectCompiled.params,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Hooks / Lifecycle Events
|
|
3
|
+
*
|
|
4
|
+
* Provides lifecycle callbacks for model events:
|
|
5
|
+
* creating, created, updating, updated, saving, saved,
|
|
6
|
+
* deleting, deleted, restoring, restored
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type ModelHookName =
|
|
10
|
+
| "creating"
|
|
11
|
+
| "created"
|
|
12
|
+
| "updating"
|
|
13
|
+
| "updated"
|
|
14
|
+
| "saving"
|
|
15
|
+
| "saved"
|
|
16
|
+
| "deleting"
|
|
17
|
+
| "deleted"
|
|
18
|
+
| "restoring"
|
|
19
|
+
| "restored";
|
|
20
|
+
|
|
21
|
+
export type ModelHookCallback<M> = (
|
|
22
|
+
model: M,
|
|
23
|
+
) => void | Promise<void> | boolean | Promise<boolean>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Model hook registry for a specific model class
|
|
27
|
+
*/
|
|
28
|
+
export class HookRunner<M> {
|
|
29
|
+
private hooks: Map<ModelHookName, ModelHookCallback<M>[]> = new Map();
|
|
30
|
+
|
|
31
|
+
constructor(private modelClass: typeof Model) {
|
|
32
|
+
this.initializeHooks();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initialize hooks from model class static definitions
|
|
37
|
+
*/
|
|
38
|
+
private initializeHooks(): void {
|
|
39
|
+
// Hooks are registered statically on the model class
|
|
40
|
+
// They're accessed via Model.on() / Model.once() methods
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Register a hook callback
|
|
45
|
+
*/
|
|
46
|
+
on(hookName: ModelHookName, callback: ModelHookCallback<M>): void {
|
|
47
|
+
if (!this.hooks.has(hookName)) {
|
|
48
|
+
this.hooks.set(hookName, []);
|
|
49
|
+
}
|
|
50
|
+
this.hooks.get(hookName)!.push(callback);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run all callbacks for a hook
|
|
55
|
+
* Returns false if any callback explicitly returns false (to abort operation)
|
|
56
|
+
*/
|
|
57
|
+
async run(hookName: ModelHookName, model: M): Promise<boolean> {
|
|
58
|
+
const callbacks = this.hooks.get(hookName) ?? [];
|
|
59
|
+
|
|
60
|
+
for (const callback of callbacks) {
|
|
61
|
+
const result = await callback(model);
|
|
62
|
+
if (result === false) {
|
|
63
|
+
return false; // Abort
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return true; // Continue
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Placeholder to avoid circular dependency issues
|
|
73
|
+
*/
|
|
74
|
+
export abstract class Model {
|
|
75
|
+
private static hookRegistry = new Map<string, Map<string, Function[]>>();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get or create the hook registry for this model class
|
|
79
|
+
*/
|
|
80
|
+
private static getHookRegistry(
|
|
81
|
+
modelName: string,
|
|
82
|
+
): Map<ModelHookName, Function[]> {
|
|
83
|
+
if (!Model.hookRegistry.has(modelName)) {
|
|
84
|
+
Model.hookRegistry.set(modelName, new Map<ModelHookName, Function[]>());
|
|
85
|
+
}
|
|
86
|
+
return Model.hookRegistry.get(modelName)! as Map<ModelHookName, Function[]>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Register a hook callback on the model class
|
|
91
|
+
*/
|
|
92
|
+
static on<M extends Model>(
|
|
93
|
+
this: { new (): M } & typeof Model,
|
|
94
|
+
hookName: ModelHookName,
|
|
95
|
+
callback: ModelHookCallback<M>,
|
|
96
|
+
): void {
|
|
97
|
+
const registry = Model.getHookRegistry(this.name);
|
|
98
|
+
if (!registry.has(hookName)) {
|
|
99
|
+
registry.set(hookName, []);
|
|
100
|
+
}
|
|
101
|
+
registry.get(hookName)!.push(callback as any);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get all callbacks for a hook
|
|
106
|
+
*/
|
|
107
|
+
static getHookCallbacks<M extends Model>(
|
|
108
|
+
this: { new (): M } & typeof Model,
|
|
109
|
+
hookName: ModelHookName,
|
|
110
|
+
): ModelHookCallback<M>[] {
|
|
111
|
+
const registry = Model.getHookRegistry(this.name);
|
|
112
|
+
return (registry.get(hookName) ?? []) as any;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bueno ORM
|
|
3
|
+
*
|
|
4
|
+
* Eloquent-inspired ORM for the Bueno framework.
|
|
5
|
+
* Provides Model, QueryBuilder, relationships, and lifecycle management.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============= Query Builder =============
|
|
9
|
+
export { OrmQueryBuilder, query } from "./builder";
|
|
10
|
+
export type { PaginationResult } from "./builder";
|
|
11
|
+
|
|
12
|
+
// ============= Model =============
|
|
13
|
+
export {
|
|
14
|
+
Model,
|
|
15
|
+
ModelNotFoundError,
|
|
16
|
+
ModelOperationAbortedError,
|
|
17
|
+
ModelQueryBuilder,
|
|
18
|
+
} from "./model";
|
|
19
|
+
|
|
20
|
+
// ============= Registry =============
|
|
21
|
+
export {
|
|
22
|
+
setDefaultDatabase,
|
|
23
|
+
getDefaultDatabase,
|
|
24
|
+
registerModelDatabase,
|
|
25
|
+
getModelDatabase,
|
|
26
|
+
clearModelDatabaseRegistry,
|
|
27
|
+
clearDefaultDatabase,
|
|
28
|
+
} from "./model-registry";
|
|
29
|
+
|
|
30
|
+
// ============= Relationships =============
|
|
31
|
+
export {
|
|
32
|
+
Relationship,
|
|
33
|
+
HasOne,
|
|
34
|
+
HasMany,
|
|
35
|
+
BelongsTo,
|
|
36
|
+
BelongsToMany,
|
|
37
|
+
} from "./relationships";
|
|
38
|
+
export type { RelationshipOptions } from "./relationships/base";
|
|
39
|
+
|
|
40
|
+
// ============= Casts =============
|
|
41
|
+
export { CastRegistry } from "./casts";
|
|
42
|
+
export type { CastDefinition, CastObject, BuiltInCastName } from "./casts";
|
|
43
|
+
|
|
44
|
+
// ============= Scopes =============
|
|
45
|
+
export { ScopeRegistry, SoftDeleteScope } from "./scopes";
|
|
46
|
+
export type { ScopeDefinition } from "./scopes";
|
|
47
|
+
|
|
48
|
+
// ============= Hooks =============
|
|
49
|
+
export { HookRunner } from "./hooks";
|
|
50
|
+
export type { ModelHookName, ModelHookCallback } from "./hooks";
|
|
51
|
+
|
|
52
|
+
// ============= Compiler (advanced usage) =============
|
|
53
|
+
export { QueryCompiler } from "./compiler";
|
|
54
|
+
export type {
|
|
55
|
+
SqlDialect,
|
|
56
|
+
CompiledQuery,
|
|
57
|
+
WhereClause,
|
|
58
|
+
OrderClause,
|
|
59
|
+
JoinClause,
|
|
60
|
+
QueryState,
|
|
61
|
+
} from "./compiler";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Registry
|
|
3
|
+
*
|
|
4
|
+
* Global registry mapping model names to their Database instances.
|
|
5
|
+
* Allows per-model connection selection and a default connection fallback.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Database } from "../index";
|
|
9
|
+
|
|
10
|
+
const registry = new Map<string, Database>();
|
|
11
|
+
let defaultDb: Database | null = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Set the default database for all models
|
|
15
|
+
*/
|
|
16
|
+
export function setDefaultDatabase(db: Database): void {
|
|
17
|
+
defaultDb = db;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get the default database
|
|
22
|
+
*/
|
|
23
|
+
export function getDefaultDatabase(): Database {
|
|
24
|
+
if (!defaultDb) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
"No default database configured. Call setDefaultDatabase() before using models.",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return defaultDb;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Register a database connection for a specific model by name
|
|
34
|
+
*/
|
|
35
|
+
export function registerModelDatabase(modelName: string, db: Database): void {
|
|
36
|
+
registry.set(modelName, db);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get the database for a model by name
|
|
41
|
+
* Falls back to default database if model-specific one not registered
|
|
42
|
+
*/
|
|
43
|
+
export function getModelDatabase(modelName: string): Database {
|
|
44
|
+
return registry.get(modelName) ?? getDefaultDatabase();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Clear all model database registrations (mainly for testing)
|
|
49
|
+
*/
|
|
50
|
+
export function clearModelDatabaseRegistry(): void {
|
|
51
|
+
registry.clear();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Clear the default database (mainly for testing)
|
|
56
|
+
*/
|
|
57
|
+
export function clearDefaultDatabase(): void {
|
|
58
|
+
defaultDb = null;
|
|
59
|
+
}
|