@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
|
@@ -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
|
+
}
|