@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
@@ -0,0 +1,146 @@
1
+ // @ts-nocheck - Abstract class generic constraints cause false positives in TypeScript.
2
+ // The implementation is logically correct; see .idea/orm-implementation-status.md
3
+
4
+ /**
5
+ * Base Relationship Class
6
+ *
7
+ * Abstract base for all relationship types (HasOne, HasMany, etc.)
8
+ */
9
+
10
+ import type { Database } from "../../index";
11
+ import { OrmQueryBuilder } from "../builder";
12
+ import type { Model } from "../model";
13
+ import { getModelDatabase } from "../model-registry";
14
+
15
+ export interface RelationshipOptions {
16
+ foreignKey?: string;
17
+ localKey?: string;
18
+ ownerKey?: string;
19
+ relatedKey?: string;
20
+ }
21
+
22
+ /**
23
+ * Abstract base relationship class
24
+ */
25
+ export abstract class Relationship<
26
+ TParent extends Model,
27
+ TRelated extends Model,
28
+ > {
29
+ protected query: OrmQueryBuilder<any>;
30
+ protected db: Database;
31
+
32
+ constructor(
33
+ protected parentModel: TParent,
34
+ protected relatedClass: { new (): TRelated } & typeof Model,
35
+ protected foreignKey: string,
36
+ protected localKey = "id",
37
+ ) {
38
+ this.db = getModelDatabase(relatedClass.name);
39
+ this.query = new OrmQueryBuilder(this.db, relatedClass.table as string);
40
+ // Note: subclasses that use private fields must call initConstraints()
41
+ // themselves after their field assignments, rather than relying on this call.
42
+ this.addConstraints();
43
+ }
44
+
45
+ /**
46
+ * Initialize constraints — call this in subclass constructors after all
47
+ * private fields are assigned, if the subclass needs them in addConstraints().
48
+ */
49
+ protected initConstraints(): void {
50
+ this.query = new OrmQueryBuilder(
51
+ this.db,
52
+ this.relatedClass.table as string,
53
+ );
54
+ this.addConstraints();
55
+ }
56
+
57
+ /**
58
+ * Add WHERE clause for this relationship
59
+ * (override in subclasses)
60
+ */
61
+ abstract addConstraints(): void;
62
+
63
+ /**
64
+ * Add eager load constraints
65
+ * (override in subclasses)
66
+ */
67
+ abstract addEagerConstraints(parents: TParent[]): void;
68
+
69
+ /**
70
+ * Match eager-loaded results to parent models
71
+ * (override in subclasses)
72
+ */
73
+ abstract match(
74
+ parents: TParent[],
75
+ results: TRelated[],
76
+ relation: string,
77
+ ): void;
78
+
79
+ /**
80
+ * Get the results for this relationship
81
+ */
82
+ abstract getResults(): Promise<TRelated | TRelated[] | null>;
83
+
84
+ /**
85
+ * Reset the query builder to a fresh state
86
+ * Used during eager loading to clear single-model constraints from constructor
87
+ */
88
+ protected resetQuery(): void {
89
+ this.query = new OrmQueryBuilder(
90
+ this.db,
91
+ this.relatedClass.table as string,
92
+ );
93
+ }
94
+
95
+ // ============= Chainable Methods =============
96
+
97
+ where(column: string, operator: unknown, value?: unknown): this {
98
+ this.query.where(column, operator, value);
99
+ return this;
100
+ }
101
+
102
+ orWhere(column: string, operator: unknown, value?: unknown): this {
103
+ this.query.orWhere(column, operator, value);
104
+ return this;
105
+ }
106
+
107
+ orderBy(column: string, direction?: "ASC" | "DESC"): this {
108
+ this.query.orderBy(column, direction);
109
+ return this;
110
+ }
111
+
112
+ limit(n: number): this {
113
+ this.query.limit(n);
114
+ return this;
115
+ }
116
+
117
+ // ============= Query Terminals =============
118
+
119
+ async get(): Promise<TRelated[]> {
120
+ const rows = await this.query.get();
121
+ return this.relatedClass.hydrate(rows) as TRelated[];
122
+ }
123
+
124
+ async first(): Promise<TRelated | null> {
125
+ const rows = await this.query.limit(1).get();
126
+ if (rows.length === 0) return null;
127
+ return this.relatedClass.hydrate([rows[0]])[0] as TRelated;
128
+ }
129
+
130
+ async count(): Promise<number> {
131
+ return this.query.count();
132
+ }
133
+
134
+ async exists(): Promise<boolean> {
135
+ return this.query.exists();
136
+ }
137
+
138
+ async create(data: Record<string, unknown>): Promise<TRelated> {
139
+ // Set the foreign key
140
+ const model_data = {
141
+ ...data,
142
+ [this.foreignKey]: this.parentModel.getAttribute(this.localKey as any),
143
+ };
144
+ return this.relatedClass.create(model_data);
145
+ }
146
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * BelongsToMany Relationship
3
+ *
4
+ * Model belongs to many related models through a pivot table (n:m)
5
+ */
6
+
7
+ import type { Database } from "../../index";
8
+ import { OrmQueryBuilder } from "../builder";
9
+ import type { Model } from "../model";
10
+ import { getModelDatabase } from "../model-registry";
11
+ import { Relationship } from "./base";
12
+
13
+ export class BelongsToMany<TRelated extends Model> extends Relationship<
14
+ any,
15
+ TRelated
16
+ > {
17
+ private pivotData: Set<string> = new Set();
18
+
19
+ constructor(
20
+ parentModel: Model,
21
+ relatedClass: { new (): TRelated } & typeof Model,
22
+ private pivotTable: string,
23
+ private foreignPivotKey: string,
24
+ private relatedPivotKey: string,
25
+ private parentKey = "id",
26
+ private relatedKey = "id",
27
+ ) {
28
+ super(parentModel, relatedClass, foreignPivotKey, parentKey);
29
+ // Private fields are assigned after super() returns, so we must
30
+ // re-initialize constraints now that they are available.
31
+ this.initConstraints();
32
+ }
33
+
34
+ addConstraints(): void {
35
+ // Guard: if pivotTable hasn't been assigned yet (called from base super()),
36
+ // do nothing — initConstraints() will call us again after field assignment.
37
+ if (!this.pivotTable) return;
38
+ const parentId = this.parentModel.getAttribute(this.parentKey as any);
39
+
40
+ this.query.join(
41
+ this.pivotTable,
42
+ `${this.pivotTable}.${this.relatedPivotKey} = ${(this.relatedClass as any).table}.${this.relatedKey}`,
43
+ );
44
+ this.query.where(`${this.pivotTable}.${this.foreignPivotKey}`, parentId);
45
+ }
46
+
47
+ addEagerConstraints(parents: Model[]): void {
48
+ const ids = parents.map((p) => p.getAttribute(this.parentKey as any));
49
+ this.query.whereIn(`${this.pivotTable}.${this.foreignPivotKey}`, ids);
50
+ }
51
+
52
+ match(parents: Model[], results: TRelated[], relation: string): void {
53
+ const grouped = new Map<unknown, TRelated[]>();
54
+
55
+ for (const result of results) {
56
+ // The JOIN puts the pivot FK column in the result row as a regular attribute
57
+ const parentId = result.getAttribute(this.foreignPivotKey as any);
58
+ if (!grouped.has(parentId)) {
59
+ grouped.set(parentId, []);
60
+ }
61
+ grouped.get(parentId)!.push(result);
62
+ }
63
+
64
+ for (const parent of parents) {
65
+ const key = parent.getAttribute(this.parentKey as any);
66
+ const related = grouped.get(key) ?? [];
67
+ (parent as any)._relations.set(relation, related);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Include specific pivot columns in results
73
+ */
74
+ withPivot(...columns: string[]): this {
75
+ for (const col of columns) {
76
+ this.pivotData.add(col);
77
+ }
78
+ return this;
79
+ }
80
+
81
+ /**
82
+ * Attach related models to the pivot table
83
+ */
84
+ async attach(
85
+ ids: unknown | unknown[],
86
+ pivotData?: Record<string, unknown>,
87
+ ): Promise<void> {
88
+ const idArray = Array.isArray(ids) ? ids : [ids];
89
+ const parentId = this.parentModel.getAttribute(this.parentKey as any);
90
+
91
+ const db = getModelDatabase(this.relatedClass.name);
92
+
93
+ for (const id of idArray) {
94
+ const data = {
95
+ [this.foreignPivotKey]: parentId,
96
+ [this.relatedPivotKey]: id,
97
+ ...pivotData,
98
+ };
99
+
100
+ // Use raw insert to avoid OrmQueryBuilder.insert()'s SELECT-by-id fetch-back,
101
+ // which fails on pivot tables that have no 'id' primary key column.
102
+ const columns = Object.keys(data).join(", ");
103
+ const placeholders = Object.keys(data)
104
+ .map(() => "?")
105
+ .join(", ");
106
+ const values = Object.values(data);
107
+ await db.raw(
108
+ `INSERT INTO ${this.pivotTable} (${columns}) VALUES (${placeholders})`,
109
+ values,
110
+ );
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Detach related models from the pivot table
116
+ */
117
+ async detach(ids?: unknown | unknown[]): Promise<void> {
118
+ const parentId = this.parentModel.getAttribute(this.parentKey as any);
119
+
120
+ const db = getModelDatabase(this.relatedClass.name);
121
+ let builder = new OrmQueryBuilder(db, this.pivotTable).where(
122
+ this.foreignPivotKey,
123
+ parentId,
124
+ );
125
+
126
+ if (ids) {
127
+ const idArray = Array.isArray(ids) ? ids : [ids];
128
+ builder = builder.whereIn(this.relatedPivotKey, idArray) as any;
129
+ }
130
+
131
+ await builder.delete();
132
+ }
133
+
134
+ /**
135
+ * Sync related models (replace all with given IDs)
136
+ */
137
+ async sync(ids: unknown[], detaching = true): Promise<void> {
138
+ if (detaching) {
139
+ await this.detach();
140
+ }
141
+ await this.attach(ids);
142
+ }
143
+
144
+ /**
145
+ * Toggle related models
146
+ */
147
+ async toggle(ids: unknown | unknown[]): Promise<void> {
148
+ const idArray = Array.isArray(ids) ? ids : [ids];
149
+ const attached = await this.get();
150
+ const attachedIds = attached.map((m) =>
151
+ m.getAttribute(this.relatedKey as any),
152
+ );
153
+
154
+ const toAttach = idArray.filter((id) => !attachedIds.includes(id));
155
+ const toDetach = attachedIds.filter((id) => idArray.includes(id));
156
+
157
+ await Promise.all([this.attach(toAttach), this.detach(toDetach)]);
158
+ }
159
+
160
+ /**
161
+ * Update pivot data
162
+ */
163
+ async updateExistingPivot(
164
+ id: unknown,
165
+ data: Record<string, unknown>,
166
+ ): Promise<void> {
167
+ const parentId = this.parentModel.getAttribute(this.parentKey as any);
168
+
169
+ const db = getModelDatabase(this.relatedClass.name);
170
+ await new OrmQueryBuilder(db, this.pivotTable)
171
+ .where(this.foreignPivotKey, parentId)
172
+ .where(this.relatedPivotKey, id)
173
+ .update(data);
174
+ }
175
+
176
+ async getResults(): Promise<TRelated[]> {
177
+ return this.get();
178
+ }
179
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * BelongsTo Relationship
3
+ *
4
+ * Model belongs to a parent model (reverse of HasOne/HasMany)
5
+ */
6
+
7
+ import type { Model } from "../model";
8
+ import { Relationship } from "./base";
9
+
10
+ export class BelongsTo<TRelated extends Model> extends Relationship<
11
+ any,
12
+ TRelated
13
+ > {
14
+ constructor(
15
+ parentModel: Model,
16
+ relatedClass: { new (): TRelated } & typeof Model,
17
+ foreignKey: string,
18
+ ownerKey = "id",
19
+ ) {
20
+ // Pass ownerKey as localKey so addConstraints() (called inside super())
21
+ // can access it via this.localKey before ownerKey is assigned as a field.
22
+ super(parentModel, relatedClass, foreignKey, ownerKey);
23
+ }
24
+
25
+ addConstraints(): void {
26
+ const parentForeignId = this.parentModel.getAttribute(
27
+ this.foreignKey as any,
28
+ );
29
+ // this.localKey holds the ownerKey value (e.g. "id")
30
+ this.query.where(this.localKey, parentForeignId);
31
+ }
32
+
33
+ addEagerConstraints(parents: Model[]): void {
34
+ const ids = parents.map((p) => p.getAttribute(this.foreignKey as any));
35
+ this.query.whereIn(this.localKey, ids);
36
+ }
37
+
38
+ match(parents: Model[], results: TRelated[], relation: string): void {
39
+ const grouped = new Map<unknown, TRelated>();
40
+
41
+ for (const result of results) {
42
+ const key = result.getAttribute(this.localKey as any);
43
+ grouped.set(key, result);
44
+ }
45
+
46
+ for (const parent of parents) {
47
+ const key = parent.getAttribute(this.foreignKey as any);
48
+ const related = grouped.get(key) ?? null;
49
+ (parent as any)._relations.set(relation, related);
50
+ }
51
+ }
52
+
53
+ async getResults(): Promise<TRelated | null> {
54
+ return this.first();
55
+ }
56
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * HasMany Relationship
3
+ *
4
+ * Model has many related models (1:n)
5
+ */
6
+
7
+ import type { Model } from "../model";
8
+ import { Relationship } from "./base";
9
+
10
+ export class HasMany<TRelated extends Model> extends Relationship<
11
+ any,
12
+ TRelated
13
+ > {
14
+ addConstraints(): void {
15
+ const parentId = this.parentModel.getAttribute(this.localKey as any);
16
+ this.query.where(this.foreignKey, parentId);
17
+ }
18
+
19
+ addEagerConstraints(parents: Model[]): void {
20
+ const ids = parents.map((p) => p.getAttribute(this.localKey as any));
21
+ this.query.whereIn(this.foreignKey, ids);
22
+ }
23
+
24
+ match(parents: Model[], results: TRelated[], relation: string): void {
25
+ const grouped = new Map<unknown, TRelated[]>();
26
+
27
+ for (const result of results) {
28
+ const key = result.getAttribute(this.foreignKey as any);
29
+ if (!grouped.has(key)) {
30
+ grouped.set(key, []);
31
+ }
32
+ grouped.get(key)!.push(result);
33
+ }
34
+
35
+ for (const parent of parents) {
36
+ const key = parent.getAttribute(this.localKey as any);
37
+ const related = grouped.get(key) ?? [];
38
+ (parent as any)._relations.set(relation, related);
39
+ }
40
+ }
41
+
42
+ async getResults(): Promise<TRelated[]> {
43
+ return this.get();
44
+ }
45
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * HasOne Relationship
3
+ *
4
+ * Model has one related model (1:1)
5
+ */
6
+
7
+ import type { Model } from "../model";
8
+ import { Relationship } from "./base";
9
+
10
+ export class HasOne<TRelated extends Model> extends Relationship<
11
+ any,
12
+ TRelated
13
+ > {
14
+ addConstraints(): void {
15
+ const parentId = this.parentModel.getAttribute(this.localKey as any);
16
+ this.query.where(this.foreignKey, parentId);
17
+ }
18
+
19
+ addEagerConstraints(parents: Model[]): void {
20
+ const ids = parents.map((p) => p.getAttribute(this.localKey as any));
21
+ this.query.whereIn(this.foreignKey, ids);
22
+ }
23
+
24
+ match(parents: Model[], results: TRelated[], relation: string): void {
25
+ const grouped = new Map<unknown, TRelated>();
26
+ for (const result of results) {
27
+ const key = result.getAttribute(this.foreignKey as any);
28
+ grouped.set(key, result);
29
+ }
30
+
31
+ for (const parent of parents) {
32
+ const key = parent.getAttribute(this.localKey as any);
33
+ const related = grouped.get(key) ?? null;
34
+ (parent as any)._relations.set(relation, related);
35
+ }
36
+ }
37
+
38
+ async getResults(): Promise<TRelated | null> {
39
+ return this.first();
40
+ }
41
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Relationship Exports
3
+ */
4
+
5
+ export { Relationship } from "./base";
6
+ export { HasOne } from "./has-one";
7
+ export { HasMany } from "./has-many";
8
+ export { BelongsTo } from "./belongs-to";
9
+ export { BelongsToMany } from "./belongs-to-many";
10
+
11
+ export type { RelationshipOptions } from "./base";
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Model Scopes
3
+ *
4
+ * Local scopes (scopeXxx methods) and global scopes (auto-applied to all queries)
5
+ */
6
+
7
+ export type ScopeDefinition<M> = (query: any) => any;
8
+
9
+ export class ScopeRegistry<M> {
10
+ private globalScopes: Map<string, ScopeDefinition<M>> = new Map();
11
+
12
+ /**
13
+ * Register a global scope
14
+ */
15
+ addGlobalScope(name: string, scope: ScopeDefinition<M>): void {
16
+ this.globalScopes.set(name, scope);
17
+ }
18
+
19
+ /**
20
+ * Remove a global scope by name
21
+ */
22
+ removeGlobalScope(name: string): void {
23
+ this.globalScopes.delete(name);
24
+ }
25
+
26
+ /**
27
+ * Get all global scopes
28
+ */
29
+ getGlobalScopes(): ScopeDefinition<M>[] {
30
+ return Array.from(this.globalScopes.values());
31
+ }
32
+
33
+ /**
34
+ * Clear all global scopes
35
+ */
36
+ clearGlobalScopes(): void {
37
+ this.globalScopes.clear();
38
+ }
39
+
40
+ /**
41
+ * Check if a global scope exists
42
+ */
43
+ hasGlobalScope(name: string): boolean {
44
+ return this.globalScopes.has(name);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Soft delete global scope — automatically added when model has softDeletes = true
50
+ */
51
+ export class SoftDeleteScope {
52
+ apply(query: any): void {
53
+ query.whereNull("deleted_at");
54
+ }
55
+ }