@zerotal/orm 1.0.0
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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/package.json +58 -0
- package/src/casts/Cast.ts +200 -0
- package/src/commands/DbSeedCommand.ts +71 -0
- package/src/commands/MakeFactoryCommand.ts +59 -0
- package/src/commands/MakeMigrationCommand.ts +109 -0
- package/src/commands/MakeModelCommand.ts +83 -0
- package/src/commands/MakeSeederCommand.ts +50 -0
- package/src/commands/MigrateCommand.ts +60 -0
- package/src/commands/MigrateFreshCommand.ts +41 -0
- package/src/commands/MigrateGenerateCommand.ts +110 -0
- package/src/commands/MigrateRollbackCommand.ts +43 -0
- package/src/commands/MigrateStatusCommand.ts +49 -0
- package/src/commands/_loadMigrations.ts +34 -0
- package/src/commands/index.ts +30 -0
- package/src/config.ts +182 -0
- package/src/conventions.ts +67 -0
- package/src/db/DB.ts +486 -0
- package/src/db/NPlusOneDetector.ts +176 -0
- package/src/db/QueryBuilder.ts +2458 -0
- package/src/db/ReadWriteRouter.ts +96 -0
- package/src/db/TransactionContext.ts +13 -0
- package/src/db/dialects/MysqlDialect.ts +57 -0
- package/src/db/dialects/PostgresDialect.ts +55 -0
- package/src/db/dialects/SqliteDialect.ts +54 -0
- package/src/db/dialects/index.ts +25 -0
- package/src/db/dialects/types.ts +67 -0
- package/src/db/resolver.ts +30 -0
- package/src/db/sql-types.ts +12 -0
- package/src/db/types.ts +296 -0
- package/src/errors/MassAssignmentError.ts +25 -0
- package/src/errors/MigrationError.ts +18 -0
- package/src/errors/ModelNotFoundError.ts +21 -0
- package/src/errors/NPlusOneError.ts +6 -0
- package/src/errors/RelationNotLoadedError.ts +19 -0
- package/src/errors/StateError.ts +18 -0
- package/src/errors/TransactionError.ts +13 -0
- package/src/errors/UnsupportedDialectError.ts +18 -0
- package/src/errors/index.ts +7 -0
- package/src/events.ts +112 -0
- package/src/global.d.ts +17 -0
- package/src/implicitBinding.ts +73 -0
- package/src/index.ts +255 -0
- package/src/model/BaseModel.ts +2499 -0
- package/src/model/ModelQueryBuilder.ts +1808 -0
- package/src/model/Observer.ts +73 -0
- package/src/model/OrmContext.ts +71 -0
- package/src/model/ReactiveProxy.ts +53 -0
- package/src/model/SoftDeletes.ts +108 -0
- package/src/model/State.ts +290 -0
- package/src/model/decorators/_metadata.ts +211 -0
- package/src/model/decorators/_registerRelation.ts +20 -0
- package/src/model/decorators/belongsTo.ts +38 -0
- package/src/model/decorators/column.ts +278 -0
- package/src/model/decorators/hasMany.ts +34 -0
- package/src/model/decorators/hasManyThrough.ts +50 -0
- package/src/model/decorators/hasOne.ts +34 -0
- package/src/model/decorators/hasOneThrough.ts +40 -0
- package/src/model/decorators/manyToMany.ts +55 -0
- package/src/model/decorators/morphMany.ts +38 -0
- package/src/model/decorators/morphOne.ts +38 -0
- package/src/model/decorators/morphTo.ts +51 -0
- package/src/model/decorators/morphToMany.ts +49 -0
- package/src/model/decorators/morphedByMany.ts +46 -0
- package/src/model/decorators/table.ts +124 -0
- package/src/model/hooks/HookRegistry.ts +110 -0
- package/src/model/mixins.ts +536 -0
- package/src/model/payload.ts +114 -0
- package/src/model/relations/RelationRegistry.ts +184 -0
- package/src/observability.ts +210 -0
- package/src/provider/DatabaseProvider.ts +266 -0
- package/src/schema/Blueprint.ts +900 -0
- package/src/schema/ColumnDefinition.ts +517 -0
- package/src/schema/Migration.ts +34 -0
- package/src/schema/MigrationCodegen.ts +108 -0
- package/src/schema/MigrationRunner.ts +351 -0
- package/src/schema/ModelInspector.ts +133 -0
- package/src/schema/Schema.ts +140 -0
- package/src/schema/SchemaDiffer.ts +137 -0
- package/src/schema/SchemaInspector.ts +164 -0
- package/src/schema/__test_migrations__/001_create_test_table.ts +15 -0
- package/src/schema/autoMigrate.ts +154 -0
- package/src/schema/index.ts +28 -0
- package/src/seeding/Seeder.ts +46 -0
- package/src/support/identifiers.ts +62 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { makeRelationDecorator } from "./_registerRelation.ts";
|
|
2
|
+
import type { HasManyThroughOptions } from "./hasManyThrough.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Declare a has-one-through relation: reach a single related record across one
|
|
6
|
+
* intermediate ("through") model (the one-to-one counterpart of
|
|
7
|
+
* {@link hasManyThrough}).
|
|
8
|
+
*
|
|
9
|
+
* @remarks Not supported by {@link has} / {@link whereHas}; use eager
|
|
10
|
+
* {@link ModelQueryBuilder.with | with()} to load it.
|
|
11
|
+
*
|
|
12
|
+
* @param related - Lazy factory returning the far/related model class.
|
|
13
|
+
* @param through - Lazy factory returning the intermediate model class.
|
|
14
|
+
* @param options - First/second key configuration across the two hops.
|
|
15
|
+
* @category Relationships
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* class Country extends BaseModel {
|
|
20
|
+
* \@hasOneThrough(() => Post, () => User, { firstKey: 'country_id', secondKey: 'user_id' })
|
|
21
|
+
* latestPost!: HasOne<Post>;
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function hasOneThrough(
|
|
26
|
+
related: () => unknown,
|
|
27
|
+
through: () => unknown,
|
|
28
|
+
options: HasManyThroughOptions,
|
|
29
|
+
) {
|
|
30
|
+
return makeRelationDecorator(() => ({
|
|
31
|
+
type: "hasOneThrough" as const,
|
|
32
|
+
related,
|
|
33
|
+
through,
|
|
34
|
+
firstKey: options.firstKey,
|
|
35
|
+
secondKey: options.secondKey,
|
|
36
|
+
foreignKey: options.secondKey,
|
|
37
|
+
localKey: options.localKey ?? "id",
|
|
38
|
+
throughLocalKey: options.throughLocalKey ?? "id",
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { makeRelationDecorator } from "./_registerRelation.ts";
|
|
2
|
+
|
|
3
|
+
/** Options for {@link manyToMany}. */
|
|
4
|
+
export interface ManyToManyOptions {
|
|
5
|
+
/** Name of the pivot (join) table (e.g. `role_user`). */
|
|
6
|
+
pivotTable: string;
|
|
7
|
+
/** Pivot column referencing this (parent) model (e.g. `user_id`). */
|
|
8
|
+
pivotForeignKey: string;
|
|
9
|
+
/** Pivot column referencing the related model (e.g. `role_id`). */
|
|
10
|
+
pivotRelatedKey: string;
|
|
11
|
+
/** Parent local key the pivot FK references. Defaults to `'id'`. */
|
|
12
|
+
localKey?: string;
|
|
13
|
+
/** Related local key the pivot relatedKey references. Defaults to `'id'`. */
|
|
14
|
+
relatedKey?: string;
|
|
15
|
+
/** Extra pivot columns to hydrate onto each related model's `pivot` bag. */
|
|
16
|
+
withPivot?: string[];
|
|
17
|
+
/** Maintain created_at / updated_at on the pivot table during attach/sync. */
|
|
18
|
+
withTimestamps?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Declare a many-to-many relation through a pivot table (e.g. `User` ↔ `Role`
|
|
23
|
+
* via `role_user`). The relation property is a {@link ManyToMany} collection that
|
|
24
|
+
* behaves as an array yet also exposes pivot methods (`attach` / `detach` /
|
|
25
|
+
* `sync` / `toggle`).
|
|
26
|
+
*
|
|
27
|
+
* @param related - Lazy factory returning the related model class.
|
|
28
|
+
* @param options - Pivot table and key configuration.
|
|
29
|
+
* @category Relationships
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* class User extends BaseModel {
|
|
34
|
+
* \@manyToMany(() => Role, {
|
|
35
|
+
* pivotTable: 'role_user',
|
|
36
|
+
* pivotForeignKey: 'user_id',
|
|
37
|
+
* pivotRelatedKey: 'role_id',
|
|
38
|
+
* })
|
|
39
|
+
* roles!: ManyToMany<Role>;
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function manyToMany(related: () => unknown, options: ManyToManyOptions) {
|
|
44
|
+
return makeRelationDecorator(() => ({
|
|
45
|
+
type: "manyToMany" as const,
|
|
46
|
+
related,
|
|
47
|
+
foreignKey: options.pivotForeignKey,
|
|
48
|
+
localKey: options.localKey ?? "id",
|
|
49
|
+
pivotTable: options.pivotTable,
|
|
50
|
+
pivotForeignKey: options.pivotForeignKey,
|
|
51
|
+
pivotRelatedKey: options.pivotRelatedKey,
|
|
52
|
+
...(options.withPivot ? { pivotColumns: options.withPivot } : {}),
|
|
53
|
+
...(options.withTimestamps ? { pivotTimestamps: options.withTimestamps } : {}),
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { makeRelationDecorator } from "./_registerRelation.ts";
|
|
2
|
+
|
|
3
|
+
export interface MorphManyOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The morph name — prefix for the `_type` and `_id` columns on the related table.
|
|
6
|
+
* e.g. `morphName: 'commentable'` → columns `commentable_type`, `commentable_id`.
|
|
7
|
+
*/
|
|
8
|
+
morphName: string;
|
|
9
|
+
/** Override the local key. Defaults to 'id'. */
|
|
10
|
+
localKey?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Declare the "one" side of a polymorphic one-to-many: the parent owns many
|
|
15
|
+
* related rows that reference it via `{morphName}_id` / `{morphName}_type`
|
|
16
|
+
* columns (e.g. a `Post` has many `Comment`s through a `commentable` morph).
|
|
17
|
+
*
|
|
18
|
+
* @param related - Lazy factory returning the related model class.
|
|
19
|
+
* @param options - The morph name and optional local key.
|
|
20
|
+
* @category Relationships
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* class Post extends BaseModel {
|
|
25
|
+
* \@morphMany(() => Comment, { morphName: 'commentable' })
|
|
26
|
+
* comments!: MorphMany<Comment>;
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function morphMany(related: () => unknown, options: MorphManyOptions) {
|
|
31
|
+
return makeRelationDecorator(() => ({
|
|
32
|
+
type: "morphMany" as const,
|
|
33
|
+
related,
|
|
34
|
+
foreignKey: `${options.morphName}_id`,
|
|
35
|
+
localKey: options.localKey ?? "id",
|
|
36
|
+
morphTypeColumn: `${options.morphName}_type`,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { makeRelationDecorator } from "./_registerRelation.ts";
|
|
2
|
+
|
|
3
|
+
export interface MorphOneOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The morph name — prefix for the `_type` and `_id` columns on the related table.
|
|
6
|
+
* e.g. `morphName: 'imageable'` → columns `imageable_type`, `imageable_id`.
|
|
7
|
+
*/
|
|
8
|
+
morphName: string;
|
|
9
|
+
/** Override the local key. Defaults to 'id'. */
|
|
10
|
+
localKey?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Declare the "one" side of a polymorphic one-to-one: the parent owns a single
|
|
15
|
+
* related row that references it via `{morphName}_id` / `{morphName}_type`
|
|
16
|
+
* columns (e.g. a `User` has one `Image` through an `imageable` morph).
|
|
17
|
+
*
|
|
18
|
+
* @param related - Lazy factory returning the related model class.
|
|
19
|
+
* @param options - The morph name and optional local key.
|
|
20
|
+
* @category Relationships
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* class User extends BaseModel {
|
|
25
|
+
* \@morphOne(() => Image, { morphName: 'imageable' })
|
|
26
|
+
* avatar!: MorphOne<Image>;
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function morphOne(related: () => unknown, options: MorphOneOptions) {
|
|
31
|
+
return makeRelationDecorator(() => ({
|
|
32
|
+
type: "morphOne" as const,
|
|
33
|
+
related,
|
|
34
|
+
foreignKey: `${options.morphName}_id`,
|
|
35
|
+
localKey: options.localKey ?? "id",
|
|
36
|
+
morphTypeColumn: `${options.morphName}_type`,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { makeRelationDecorator } from "./_registerRelation.ts";
|
|
2
|
+
|
|
3
|
+
export interface MorphToOptions {
|
|
4
|
+
/** Maps the discriminator string stored in the _type column to the model factory. */
|
|
5
|
+
morphMap: Record<string, () => unknown>;
|
|
6
|
+
/**
|
|
7
|
+
* Override the _type column name. Defaults to `<propertyName>_type`.
|
|
8
|
+
* e.g. property `commentable` → `commentable_type`.
|
|
9
|
+
*/
|
|
10
|
+
morphTypeColumn?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Override the _id column name. Defaults to `<propertyName>_id`.
|
|
13
|
+
* e.g. property `commentable` → `commentable_id`.
|
|
14
|
+
*/
|
|
15
|
+
morphForeignKey?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function _toSnake(s: string): string {
|
|
19
|
+
return s.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Declare the inverse side of a polymorphic relation: this model belongs to one
|
|
24
|
+
* of several possible parent types, resolved at load time from the `_type`
|
|
25
|
+
* discriminator column via the {@link MorphToOptions.morphMap | morphMap}
|
|
26
|
+
* (e.g. a `Comment` belongs to a `Post` or `Video` through `commentable`).
|
|
27
|
+
*
|
|
28
|
+
* @param options - The type-to-model map plus optional column-name overrides.
|
|
29
|
+
* @category Relationships
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* class Comment extends BaseModel {
|
|
34
|
+
* \@morphTo({ morphMap: { Post: () => Post, Video: () => Video } })
|
|
35
|
+
* commentable!: MorphTo<Post | Video>;
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function morphTo(options: MorphToOptions) {
|
|
40
|
+
return makeRelationDecorator((_ctor, field) => {
|
|
41
|
+
const snake = _toSnake(field);
|
|
42
|
+
return {
|
|
43
|
+
type: "morphTo" as const,
|
|
44
|
+
related: () => ({}),
|
|
45
|
+
foreignKey: options.morphForeignKey ?? `${snake}_id`,
|
|
46
|
+
localKey: "id",
|
|
47
|
+
morphMap: options.morphMap,
|
|
48
|
+
morphTypeColumn: options.morphTypeColumn ?? `${snake}_type`,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { makeRelationDecorator } from "./_registerRelation.ts";
|
|
2
|
+
|
|
3
|
+
export interface MorphToManyOptions {
|
|
4
|
+
/** Morph name — prefix for the pivot `{name}_id` / `{name}_type` columns (e.g. 'taggable'). */
|
|
5
|
+
morphName: string;
|
|
6
|
+
/** FK on the pivot table referencing the related model (e.g. 'tag_id'). */
|
|
7
|
+
relatedPivotKey: string;
|
|
8
|
+
/** Pivot table name. Defaults to `{morphName}s`. */
|
|
9
|
+
pivotTable?: string;
|
|
10
|
+
/** Extra pivot columns to hydrate. */
|
|
11
|
+
withPivot?: string[];
|
|
12
|
+
/** Maintain pivot timestamps. */
|
|
13
|
+
withTimestamps?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Declare a polymorphic many-to-many from the "owning" side: several parent types
|
|
18
|
+
* share one pivot table that carries a `_type` discriminator, linking each to the
|
|
19
|
+
* related model (e.g. `Post` / `Video` —(taggables)→ `Tag`). The property is a
|
|
20
|
+
* {@link ManyToMany} collection with pivot methods.
|
|
21
|
+
*
|
|
22
|
+
* @param related - Lazy factory returning the related model class.
|
|
23
|
+
* @param options - Morph name, related pivot key, and optional pivot config.
|
|
24
|
+
* @category Relationships
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* class Post extends BaseModel {
|
|
29
|
+
* \@morphToMany(() => Tag, { morphName: 'taggable', relatedPivotKey: 'tag_id' })
|
|
30
|
+
* tags!: ManyToMany<Tag>;
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function morphToMany(related: () => unknown, options: MorphToManyOptions) {
|
|
35
|
+
const pivotTable = options.pivotTable ?? `${options.morphName}s`;
|
|
36
|
+
return makeRelationDecorator((ctor) => ({
|
|
37
|
+
type: "morphToMany" as const,
|
|
38
|
+
related,
|
|
39
|
+
pivotTable,
|
|
40
|
+
pivotForeignKey: `${options.morphName}_id`,
|
|
41
|
+
pivotRelatedKey: options.relatedPivotKey,
|
|
42
|
+
pivotMorphType: `${options.morphName}_type`,
|
|
43
|
+
pivotMorphValue: ctor.name,
|
|
44
|
+
foreignKey: `${options.morphName}_id`,
|
|
45
|
+
localKey: "id",
|
|
46
|
+
...(options.withPivot ? { pivotColumns: options.withPivot } : {}),
|
|
47
|
+
...(options.withTimestamps ? { pivotTimestamps: options.withTimestamps } : {}),
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { makeRelationDecorator } from "./_registerRelation.ts";
|
|
2
|
+
|
|
3
|
+
export interface MorphedByManyOptions {
|
|
4
|
+
/** Morph name — the pivot `{name}_id` / `{name}_type` columns describe the related side. */
|
|
5
|
+
morphName: string;
|
|
6
|
+
/** FK on the pivot referencing the parent model (e.g. 'tag_id'). */
|
|
7
|
+
parentPivotKey: string;
|
|
8
|
+
/** Pivot table name. Defaults to `{morphName}s`. */
|
|
9
|
+
pivotTable?: string;
|
|
10
|
+
withPivot?: string[];
|
|
11
|
+
withTimestamps?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Declare a polymorphic many-to-many from the "inverse" side: the shared model
|
|
16
|
+
* reaches back to each of the owning types through the same morph pivot table
|
|
17
|
+
* (e.g. `Tag` —(taggables)→ `Post` / `Video`). The property is a
|
|
18
|
+
* {@link ManyToMany} collection with pivot methods.
|
|
19
|
+
*
|
|
20
|
+
* @param related - Lazy factory returning the related (owning) model class.
|
|
21
|
+
* @param options - Morph name, parent pivot key, and optional pivot config.
|
|
22
|
+
* @category Relationships
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* class Tag extends BaseModel {
|
|
27
|
+
* \@morphedByMany(() => Post, { morphName: 'taggable', parentPivotKey: 'tag_id' })
|
|
28
|
+
* posts!: ManyToMany<Post>;
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function morphedByMany(related: () => unknown, options: MorphedByManyOptions) {
|
|
33
|
+
const pivotTable = options.pivotTable ?? `${options.morphName}s`;
|
|
34
|
+
return makeRelationDecorator(() => ({
|
|
35
|
+
type: "morphedByMany" as const,
|
|
36
|
+
related,
|
|
37
|
+
pivotTable,
|
|
38
|
+
pivotForeignKey: options.parentPivotKey,
|
|
39
|
+
pivotRelatedKey: `${options.morphName}_id`,
|
|
40
|
+
pivotMorphType: `${options.morphName}_type`,
|
|
41
|
+
foreignKey: options.parentPivotKey,
|
|
42
|
+
localKey: "id",
|
|
43
|
+
...(options.withPivot ? { pivotColumns: options.withPivot } : {}),
|
|
44
|
+
...(options.withTimestamps ? { pivotTimestamps: options.withTimestamps } : {}),
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { drainPendingMembers } from "./_metadata.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options object accepted as the second argument to `@table()`.
|
|
5
|
+
* Equivalent to calling the fluent chain methods — choose whichever style you prefer.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* @table("users", { timestamps: true })
|
|
9
|
+
* export class User extends BaseModel { ... }
|
|
10
|
+
*/
|
|
11
|
+
export interface TableOptions {
|
|
12
|
+
/** Enable / disable automatic timestamp columns. @default true */
|
|
13
|
+
timestamps?: boolean;
|
|
14
|
+
/** Override the primary key column name. @default "id" */
|
|
15
|
+
primaryKey?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Configuration collected by the fluent builder.
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
interface TableConfig {
|
|
23
|
+
tableName: string;
|
|
24
|
+
timestamps: boolean;
|
|
25
|
+
primaryKey: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A function that acts as a class decorator AND exposes chainable configuration
|
|
30
|
+
* methods. Every chain method returns the same builder so you can keep chaining
|
|
31
|
+
* or apply it directly as a decorator.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // Fluent chain form
|
|
35
|
+
* @table("users").withoutTimestamps()
|
|
36
|
+
* export class User extends BaseModel { ... }
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // Options-object form — same result, no parens needed on the decorator
|
|
40
|
+
* @table("users", { timestamps: true })
|
|
41
|
+
* export class User extends BaseModel { ... }
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // Non-standard primary key
|
|
45
|
+
* @table("posts").primaryKey("post_id")
|
|
46
|
+
* export class Post extends BaseModel { ... }
|
|
47
|
+
*
|
|
48
|
+
* Soft deletes are opt-in via the `SoftDeletes` mixin, not `@table`:
|
|
49
|
+
* `class Post extends BaseModelWith(SoftDeletes) {}` — see /docs/orm/lifecycle.
|
|
50
|
+
*/
|
|
51
|
+
export interface TableDecoratorBuilder {
|
|
52
|
+
/** Apply the decorator to a class constructor (called automatically by TS). */
|
|
53
|
+
(target: Function, context?: unknown): void;
|
|
54
|
+
|
|
55
|
+
/** Enable automatic `created_at` / `updated_at` management. Default: on. */
|
|
56
|
+
withTimestamps(): TableDecoratorBuilder;
|
|
57
|
+
/** Disable automatic timestamp columns. */
|
|
58
|
+
withoutTimestamps(): TableDecoratorBuilder;
|
|
59
|
+
/** Override the primary key column name. Default: "id". */
|
|
60
|
+
primaryKey(key: string): TableDecoratorBuilder;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Fluent class decorator factory for configuring a `BaseModel` subclass.
|
|
65
|
+
*
|
|
66
|
+
* Accepts an optional `options` object as a second argument so you don't need to
|
|
67
|
+
* chain methods if you prefer the inline form. Both styles are fully equivalent.
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* // Style A — fluent chain
|
|
71
|
+
* @table("users").withoutTimestamps()
|
|
72
|
+
* export class User extends BaseModel { ... }
|
|
73
|
+
*
|
|
74
|
+
* // Style B — options object
|
|
75
|
+
* @table("users", { timestamps: true })
|
|
76
|
+
* export class User extends BaseModel { ... }
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* Timestamps are **on by default** — use `.withoutTimestamps()` to opt out. Soft
|
|
80
|
+
* deletes are **opt-in via the `SoftDeletes` mixin**: `extends BaseModelWith(SoftDeletes)`.
|
|
81
|
+
*
|
|
82
|
+
* `@table` is the single, required way to configure a model: besides setting the
|
|
83
|
+
* table name and options, it anchors the class's `@column`/relation registrations at
|
|
84
|
+
* definition time (the decorator runs synchronously with the class in hand). Any class
|
|
85
|
+
* that declares column or relation fields — including a subclass that ADDS fields — must
|
|
86
|
+
* carry its own `@table`, or those fields won't register.
|
|
87
|
+
*/
|
|
88
|
+
export function table(tableName: string, options: TableOptions = {}): TableDecoratorBuilder {
|
|
89
|
+
const config: TableConfig = {
|
|
90
|
+
tableName,
|
|
91
|
+
timestamps: options.timestamps ?? true,
|
|
92
|
+
primaryKey: options.primaryKey ?? "id",
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
function apply(target: Function, _context?: unknown): void {
|
|
96
|
+
// Drain the @column / @relation registrations queued while this class's members were
|
|
97
|
+
// decorated. @table is the definition-time anchor that owns this — see _metadata.ts.
|
|
98
|
+
drainPendingMembers(target);
|
|
99
|
+
const ctor = target as unknown as Record<string, unknown>;
|
|
100
|
+
ctor.table = config.tableName;
|
|
101
|
+
ctor.timestamps = config.timestamps;
|
|
102
|
+
ctor.primaryKey = config.primaryKey;
|
|
103
|
+
// NOTE: `softDeletes` is intentionally NOT set here — it's owned by the SoftDeletes
|
|
104
|
+
// mixin (`static softDeletes = true`). Setting it from @table would clobber the
|
|
105
|
+
// mixin's value back to the default on models that compose it.
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
apply.withTimestamps = (): TableDecoratorBuilder => {
|
|
109
|
+
config.timestamps = true;
|
|
110
|
+
return apply as TableDecoratorBuilder;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
apply.withoutTimestamps = (): TableDecoratorBuilder => {
|
|
114
|
+
config.timestamps = false;
|
|
115
|
+
return apply as TableDecoratorBuilder;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
apply.primaryKey = (key: string): TableDecoratorBuilder => {
|
|
119
|
+
config.primaryKey = key;
|
|
120
|
+
return apply as TableDecoratorBuilder;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return apply as TableDecoratorBuilder;
|
|
124
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { currentOrmContext } from "../OrmContext.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The set of model lifecycle points a hook can attach to. Each fires once per
|
|
6
|
+
* persistence operation; `beforeSave`/`afterSave` wrap both inserts and updates,
|
|
7
|
+
* and `afterFind` fires when a row is hydrated from the database.
|
|
8
|
+
*/
|
|
9
|
+
export type HookName =
|
|
10
|
+
| "beforeCreate"
|
|
11
|
+
| "afterCreate"
|
|
12
|
+
| "beforeSave"
|
|
13
|
+
| "afterSave"
|
|
14
|
+
| "beforeUpdate"
|
|
15
|
+
| "afterUpdate"
|
|
16
|
+
| "beforeDelete"
|
|
17
|
+
| "afterDelete"
|
|
18
|
+
| "afterFind";
|
|
19
|
+
|
|
20
|
+
type HookFn<T> = (model: T) => Promise<void> | void;
|
|
21
|
+
|
|
22
|
+
function _registry(): Map<Function, Map<HookName, HookFn<unknown>[]>> {
|
|
23
|
+
return currentOrmContext().hooks as unknown as Map<Function, Map<HookName, HookFn<unknown>[]>>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* AsyncLocalStorage carrying a boolean that, when `true`, mutes all hook
|
|
28
|
+
* and observer execution for the duration of the callback.
|
|
29
|
+
*
|
|
30
|
+
* Set by `_suppressHooks()` — used by factories to silence observers during
|
|
31
|
+
* seeding. Opt out by calling `.dispatchEvents()` on the factory.
|
|
32
|
+
*/
|
|
33
|
+
const _suppressCtx = new AsyncLocalStorage<true>();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Run `fn` with all model hooks and observers silenced.
|
|
37
|
+
* Used internally by Factory when `dispatchEvents()` has NOT been called.
|
|
38
|
+
*/
|
|
39
|
+
export function _suppressHooks<T>(fn: () => Promise<T>): Promise<T> {
|
|
40
|
+
return _suppressCtx.run(true, fn);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Central store of per-model lifecycle hook callbacks, keyed by model class and
|
|
45
|
+
* {@link HookName}. Hooks registered on a parent class also run for subclasses
|
|
46
|
+
* (the prototype chain is walked in base-to-derived order).
|
|
47
|
+
*
|
|
48
|
+
* Registration is driven by higher-level APIs (model `beforeCreate()` etc. and the
|
|
49
|
+
* observer bridge in {@link registerObserver}); most application code never calls
|
|
50
|
+
* this class directly.
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
export class HookRegistry {
|
|
55
|
+
/**
|
|
56
|
+
* Optional post-run callback, invoked after a hook's functions run (and only when hooks
|
|
57
|
+
* are not suppressed). BaseModel sets this to dispatch model events (`dispatchesEvents`).
|
|
58
|
+
*/
|
|
59
|
+
static onAfterRun: ((ModelClass: Function, hook: HookName, model: unknown) => void) | undefined;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Append a hook callback for a model class at a given lifecycle point.
|
|
63
|
+
*
|
|
64
|
+
* @param ModelClass - The model constructor the hook belongs to.
|
|
65
|
+
* @param hook - Which lifecycle point to fire on.
|
|
66
|
+
* @param fn - Callback receiving the model instance; may be async.
|
|
67
|
+
*/
|
|
68
|
+
static register<T>(ModelClass: Function, hook: HookName, fn: HookFn<T>): void {
|
|
69
|
+
const registry = _registry();
|
|
70
|
+
if (!registry.has(ModelClass)) {
|
|
71
|
+
registry.set(ModelClass, new Map());
|
|
72
|
+
}
|
|
73
|
+
const hooks = registry.get(ModelClass)!;
|
|
74
|
+
if (!hooks.has(hook)) hooks.set(hook, []);
|
|
75
|
+
hooks.get(hook)!.push(fn as HookFn<unknown>);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Run every registered callback for `hook` on `ModelClass`, awaiting each in
|
|
80
|
+
* turn, walking the prototype chain base-to-derived. No-op while hooks are
|
|
81
|
+
* suppressed (see {@link _suppressHooks}); otherwise invokes `onAfterRun` afterward.
|
|
82
|
+
*
|
|
83
|
+
* @param ModelClass - The model constructor whose hooks (and inherited hooks) to run.
|
|
84
|
+
* @param hook - Which lifecycle point is firing.
|
|
85
|
+
* @param model - The model instance passed to each callback.
|
|
86
|
+
*/
|
|
87
|
+
static async run<T>(ModelClass: Function, hook: HookName, model: T): Promise<void> {
|
|
88
|
+
if (_suppressCtx.getStore()) return;
|
|
89
|
+
|
|
90
|
+
// Walk the prototype chain to collect inherited hooks
|
|
91
|
+
const chain: Function[] = [];
|
|
92
|
+
let cur: Function | null = ModelClass;
|
|
93
|
+
while (cur && cur !== Function.prototype) {
|
|
94
|
+
chain.unshift(cur);
|
|
95
|
+
cur = Object.getPrototypeOf(cur) as Function | null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const registry = _registry();
|
|
99
|
+
for (const cls of chain) {
|
|
100
|
+
const fns = registry.get(cls)?.get(hook);
|
|
101
|
+
if (fns) {
|
|
102
|
+
for (const fn of fns) {
|
|
103
|
+
await fn(model as unknown);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
HookRegistry.onAfterRun?.(ModelClass, hook, model as unknown);
|
|
109
|
+
}
|
|
110
|
+
}
|