@tsed/objection 8.0.0 → 8.0.2

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 (42) hide show
  1. package/package.json +14 -13
  2. package/src/components/createBooleanColumn.spec.ts +20 -0
  3. package/src/components/createBooleanColumn.ts +12 -0
  4. package/src/components/createIdColumn.spec.ts +45 -0
  5. package/src/components/createIdColumn.ts +23 -0
  6. package/src/components/createNumberColumn.spec.ts +34 -0
  7. package/src/components/createNumberColumn.ts +13 -0
  8. package/src/components/createStringColumn.spec.ts +34 -0
  9. package/src/components/createStringColumn.ts +13 -0
  10. package/src/decorators/belongsToOne.spec.ts +60 -0
  11. package/src/decorators/belongsToOne.ts +14 -0
  12. package/src/decorators/columnOptions.ts +13 -0
  13. package/src/decorators/decimal.spec.ts +52 -0
  14. package/src/decorators/decimal.ts +35 -0
  15. package/src/decorators/entity.spec.ts +24 -0
  16. package/src/decorators/entity.ts +29 -0
  17. package/src/decorators/hasMany.spec.ts +60 -0
  18. package/src/decorators/hasMany.ts +14 -0
  19. package/src/decorators/hasOne.spec.ts +60 -0
  20. package/src/decorators/hasOne.ts +14 -0
  21. package/src/decorators/hasOneThroughRelation.spec.ts +72 -0
  22. package/src/decorators/hasOneThroughRelation.ts +14 -0
  23. package/src/decorators/idColumn.spec.ts +31 -0
  24. package/src/decorators/idColumn.ts +26 -0
  25. package/src/decorators/manyToMany.spec.ts +72 -0
  26. package/src/decorators/manyToMany.ts +14 -0
  27. package/src/decorators/relatesTo.spec.ts +34 -0
  28. package/src/decorators/relatesTo.ts +26 -0
  29. package/src/domain/ColumnOpts.ts +4 -0
  30. package/src/domain/RelationshipOpts.ts +14 -0
  31. package/src/domain/interfaces.ts +9 -0
  32. package/src/index.ts +29 -0
  33. package/src/services/ColumnTypesContainer.ts +8 -0
  34. package/src/services/ObjectionConnection.ts +26 -0
  35. package/src/utils/connect.ts +9 -0
  36. package/src/utils/createColumns.spec.ts +79 -0
  37. package/src/utils/createColumns.ts +39 -0
  38. package/src/utils/createJoinKeys.ts +38 -0
  39. package/src/utils/createRelationshipMapping.ts +21 -0
  40. package/src/utils/defineStaticGetter.ts +10 -0
  41. package/src/utils/getColumnCtx.ts +21 -0
  42. package/src/utils/getJsonEntityRelationships.ts +16 -0
package/package.json CHANGED
@@ -2,13 +2,14 @@
2
2
  "name": "@tsed/objection",
3
3
  "description": "Objection.js package for Ts.ED framework",
4
4
  "type": "module",
5
- "version": "8.0.0",
5
+ "version": "8.0.2",
6
6
  "source": "./src/index.ts",
7
7
  "main": "./lib/esm/index.js",
8
8
  "module": "./lib/esm/index.js",
9
9
  "typings": "./lib/types/index.d.ts",
10
10
  "exports": {
11
11
  ".": {
12
+ "@tsed/source": "./src/index.ts",
12
13
  "types": "./lib/types/index.d.ts",
13
14
  "import": "./lib/esm/index.js",
14
15
  "default": "./lib/esm/index.js"
@@ -25,13 +26,13 @@
25
26
  "tslib": "2.7.0"
26
27
  },
27
28
  "devDependencies": {
28
- "@tsed/ajv": "8.0.0",
29
- "@tsed/barrels": "8.0.0",
30
- "@tsed/core": "8.0.0",
31
- "@tsed/di": "8.0.0",
32
- "@tsed/json-mapper": "8.0.0",
33
- "@tsed/schema": "8.0.0",
34
- "@tsed/typescript": "8.0.0",
29
+ "@tsed/ajv": "8.0.2",
30
+ "@tsed/barrels": "8.0.2",
31
+ "@tsed/core": "8.0.2",
32
+ "@tsed/di": "8.0.2",
33
+ "@tsed/json-mapper": "8.0.2",
34
+ "@tsed/schema": "8.0.2",
35
+ "@tsed/typescript": "8.0.2",
35
36
  "eslint": "9.12.0",
36
37
  "knex": "^3.1.0",
37
38
  "objection": "^2.2.18",
@@ -40,11 +41,11 @@
40
41
  "vitest": "2.1.2"
41
42
  },
42
43
  "peerDependencies": {
43
- "@tsed/ajv": "8.0.0",
44
- "@tsed/core": "8.0.0",
45
- "@tsed/di": "8.0.0",
46
- "@tsed/json-mapper": "8.0.0",
47
- "@tsed/schema": "8.0.0",
44
+ "@tsed/ajv": "8.0.2",
45
+ "@tsed/core": "8.0.2",
46
+ "@tsed/di": "8.0.2",
47
+ "@tsed/json-mapper": "8.0.2",
48
+ "@tsed/schema": "8.0.2",
48
49
  "knex": ">=0.94.0",
49
50
  "objection": ">=2.0.0"
50
51
  },
@@ -0,0 +1,20 @@
1
+ import {Property} from "@tsed/schema";
2
+
3
+ import {createTableStub} from "../../test/helpers/knex/table.js";
4
+ import {Entity} from "../decorators/entity.js";
5
+ import {createColumns} from "../utils/createColumns.js";
6
+
7
+ describe("createBooleanColumn", () => {
8
+ it("should create table from a given class", () => {
9
+ @Entity("users")
10
+ class User {
11
+ @Property()
12
+ activated: boolean;
13
+ }
14
+
15
+ const table = createTableStub();
16
+ createColumns(table, User);
17
+
18
+ expect(table.boolean).toHaveBeenCalledWith("activated");
19
+ });
20
+ });
@@ -0,0 +1,12 @@
1
+ import type {Knex} from "knex";
2
+
3
+ import {ColumnTypesContainer} from "../services/ColumnTypesContainer.js";
4
+ import {ColumnCtx} from "../utils/getColumnCtx.js";
5
+ /**
6
+ * @ignore
7
+ */
8
+ export function createBooleanColumn(table: Knex.TableBuilder, {entity}: ColumnCtx) {
9
+ table.boolean(entity.propertyName);
10
+ }
11
+
12
+ ColumnTypesContainer.set("boolean", createBooleanColumn);
@@ -0,0 +1,45 @@
1
+ import {createTableStub} from "../../test/helpers/knex/table.js";
2
+ import {Entity} from "../decorators/entity.js";
3
+ import {IdColumn} from "../decorators/idColumn.js";
4
+ import {createColumns} from "../utils/createColumns.js";
5
+
6
+ describe("createIdColumn", () => {
7
+ it("should create table from a given class (bigIncrements)", () => {
8
+ @Entity("users")
9
+ class User {
10
+ @IdColumn("bigIncrements")
11
+ id: number;
12
+ }
13
+
14
+ const table = createTableStub();
15
+ createColumns(table, User);
16
+
17
+ expect(table.bigIncrements).toHaveBeenCalledWith("id");
18
+ });
19
+
20
+ it("should create table from a given class (increments)", () => {
21
+ @Entity("users")
22
+ class User {
23
+ @IdColumn("increments")
24
+ id: number;
25
+ }
26
+
27
+ const table = createTableStub();
28
+ createColumns(table, User);
29
+
30
+ expect(table.increments).toHaveBeenCalledWith("id");
31
+ });
32
+
33
+ it("should create table from a given class (uuid)", () => {
34
+ @Entity("users")
35
+ class User {
36
+ @IdColumn("uuid")
37
+ id: string;
38
+ }
39
+
40
+ const table = createTableStub();
41
+ createColumns(table, User);
42
+
43
+ expect(table.uuid).toHaveBeenCalledWith("id");
44
+ });
45
+ });
@@ -0,0 +1,23 @@
1
+ import {randomUUID} from "crypto";
2
+ import type {Knex} from "knex";
3
+
4
+ import {ColumnTypesContainer} from "../services/ColumnTypesContainer.js";
5
+ import {ColumnCtx} from "../utils/getColumnCtx.js";
6
+
7
+ /**
8
+ * @ignore
9
+ */
10
+ export function createIdColumn(table: Knex.TableBuilder, {entity, options}: ColumnCtx) {
11
+ switch (options.type) {
12
+ case "bigIncrements":
13
+ table.bigIncrements(entity.propertyName).primary();
14
+ break;
15
+ case "increments":
16
+ table.increments(entity.propertyName).primary();
17
+ break;
18
+ case "uuid":
19
+ table.uuid(entity.propertyName).primary().defaultTo(randomUUID());
20
+ }
21
+ }
22
+
23
+ ColumnTypesContainer.set("idColumn", createIdColumn);
@@ -0,0 +1,34 @@
1
+ import {Property} from "@tsed/schema";
2
+
3
+ import {createTableStub} from "../../test/helpers/knex/table.js";
4
+ import {Decimal} from "../decorators/decimal.js";
5
+ import {Entity} from "../decorators/entity.js";
6
+ import {createColumns} from "../utils/createColumns.js";
7
+
8
+ describe("createNumberColumn", () => {
9
+ it("should create table from a given class (default)", async () => {
10
+ @Entity("users")
11
+ class User {
12
+ @Property()
13
+ prop: number;
14
+ }
15
+
16
+ const table = createTableStub();
17
+ await createColumns(table, User);
18
+
19
+ expect(table.decimal).toHaveBeenCalledWith("prop", undefined, undefined);
20
+ });
21
+
22
+ it("should create table from a given class (decimal)", async () => {
23
+ @Entity("users")
24
+ class User {
25
+ @Decimal({scale: 1, precision: 12})
26
+ prop: number;
27
+ }
28
+
29
+ const table = createTableStub();
30
+ await createColumns(table, User);
31
+
32
+ expect(table.decimal).toHaveBeenCalledWith("prop", 12, 1);
33
+ });
34
+ });
@@ -0,0 +1,13 @@
1
+ import type {Knex} from "knex";
2
+
3
+ import {ColumnTypesContainer} from "../services/ColumnTypesContainer.js";
4
+ import {ColumnCtx} from "../utils/getColumnCtx.js";
5
+
6
+ /**
7
+ * @ignore
8
+ */
9
+ export function createNumberColumn(table: Knex.TableBuilder, {entity, options}: ColumnCtx) {
10
+ table.decimal(entity.propertyName, options.precision, options.scale);
11
+ }
12
+
13
+ ColumnTypesContainer.set("number", createNumberColumn);
@@ -0,0 +1,34 @@
1
+ import {MaxLength, Property} from "@tsed/schema";
2
+
3
+ import {createTableStub} from "../../test/helpers/knex/table.js";
4
+ import {Entity} from "../decorators/entity.js";
5
+ import {createColumns} from "../utils/createColumns.js";
6
+
7
+ describe("createNumberColumn", () => {
8
+ it("should create table from a given class (with maxLength)", async () => {
9
+ @Entity("users")
10
+ class User {
11
+ @Property()
12
+ @MaxLength(200)
13
+ prop: string;
14
+ }
15
+
16
+ const table = createTableStub();
17
+ await createColumns(table, User);
18
+
19
+ expect(table.string).toHaveBeenCalledWith("prop", 200);
20
+ });
21
+
22
+ it("should create table from a given class (without maxLength)", async () => {
23
+ @Entity("users")
24
+ class User {
25
+ @Property()
26
+ prop: string;
27
+ }
28
+
29
+ const table = createTableStub();
30
+ await createColumns(table, User);
31
+
32
+ expect(table.string).toHaveBeenCalledWith("prop", undefined);
33
+ });
34
+ });
@@ -0,0 +1,13 @@
1
+ import type {Knex} from "knex";
2
+
3
+ import {ColumnTypesContainer} from "../services/ColumnTypesContainer.js";
4
+ import {ColumnCtx} from "../utils/getColumnCtx.js";
5
+
6
+ /**
7
+ * @ignore
8
+ */
9
+ export function createStringColumn(table: Knex.TableBuilder, {entity, schema}: ColumnCtx) {
10
+ table.string(entity.propertyName, schema.maxLength);
11
+ }
12
+
13
+ ColumnTypesContainer.set("string", createStringColumn);
@@ -0,0 +1,60 @@
1
+ import {Model} from "objection";
2
+
3
+ import {BelongsToOne} from "./belongsToOne.js";
4
+ import {Entity} from "./entity.js";
5
+ import {IdColumn} from "./idColumn.js";
6
+
7
+ describe("@BelongsToOne", () => {
8
+ it("should set metadata", () => {
9
+ @Entity("user")
10
+ class User extends Model {
11
+ @IdColumn()
12
+ id!: string;
13
+ }
14
+
15
+ @Entity("movie")
16
+ class Movie extends Model {
17
+ @IdColumn()
18
+ id!: string;
19
+ userId!: string;
20
+ @BelongsToOne()
21
+ user?: User;
22
+ }
23
+ expect(Movie.relationMappings).toEqual({
24
+ user: {
25
+ relation: Model.BelongsToOneRelation,
26
+ modelClass: User,
27
+ join: {
28
+ from: "movie.userId",
29
+ to: "user.id"
30
+ }
31
+ }
32
+ });
33
+ });
34
+ it("should set custom relationship path", () => {
35
+ @Entity("user")
36
+ class User extends Model {
37
+ @IdColumn()
38
+ id!: string;
39
+ userId!: string;
40
+ }
41
+ @Entity("movie")
42
+ class Movie extends Model {
43
+ @IdColumn()
44
+ id!: string;
45
+ ownerId!: string;
46
+ @BelongsToOne({from: "ownerId", to: "userId"})
47
+ user?: User;
48
+ }
49
+ expect(Movie.relationMappings).toEqual({
50
+ user: {
51
+ relation: Model.BelongsToOneRelation,
52
+ modelClass: User,
53
+ join: {
54
+ from: "movie.ownerId",
55
+ to: "user.userId"
56
+ }
57
+ }
58
+ });
59
+ });
60
+ });
@@ -0,0 +1,14 @@
1
+ import {Model} from "objection";
2
+
3
+ import {RelationshipOptsWithoutThrough} from "../domain/RelationshipOpts.js";
4
+ import {RelatesTo} from "./relatesTo.js";
5
+
6
+ /**
7
+ *
8
+ * @param opts
9
+ * @decorator
10
+ * @objection
11
+ */
12
+ export function BelongsToOne(opts?: RelationshipOptsWithoutThrough): PropertyDecorator {
13
+ return RelatesTo(Model.BelongsToOneRelation, opts);
14
+ }
@@ -0,0 +1,13 @@
1
+ import {StoreSet} from "@tsed/core";
2
+
3
+ import type {ColumnOpts} from "../domain/ColumnOpts.js";
4
+
5
+ /**
6
+ *
7
+ * @param options
8
+ * @decorator
9
+ * @objection
10
+ */
11
+ export function ColumnOptions(options: Partial<ColumnOpts>): PropertyDecorator {
12
+ return StoreSet("objection", options) as any;
13
+ }
@@ -0,0 +1,52 @@
1
+ import {Store} from "@tsed/core";
2
+ import {getJsonSchema} from "@tsed/schema";
3
+
4
+ import {Decimal} from "./decimal.js";
5
+
6
+ describe("@Decimal", () => {
7
+ it("should set metadata", () => {
8
+ class MyModel {
9
+ @Decimal({scale: 1, precision: 12})
10
+ prop: number;
11
+ }
12
+
13
+ expect(getJsonSchema(MyModel)).toEqual({
14
+ properties: {
15
+ prop: {
16
+ type: "number"
17
+ }
18
+ },
19
+ type: "object"
20
+ });
21
+ expect(Store.from(MyModel, "prop").get("objection")).toEqual({
22
+ columnType: "number",
23
+ options: {
24
+ precision: 12,
25
+ scale: 1,
26
+ type: "decimal"
27
+ }
28
+ });
29
+ });
30
+
31
+ it("should set metadata (without options)", () => {
32
+ class MyModel {
33
+ @Decimal()
34
+ prop: number;
35
+ }
36
+
37
+ expect(getJsonSchema(MyModel)).toEqual({
38
+ properties: {
39
+ prop: {
40
+ type: "number"
41
+ }
42
+ },
43
+ type: "object"
44
+ });
45
+ expect(Store.from(MyModel, "prop").get("objection")).toEqual({
46
+ columnType: "number",
47
+ options: {
48
+ type: "decimal"
49
+ }
50
+ });
51
+ });
52
+ });
@@ -0,0 +1,35 @@
1
+ import {StoreSet, useDecorators} from "@tsed/core";
2
+ import {Property} from "@tsed/schema";
3
+
4
+ import {ColumnOptions} from "./columnOptions.js";
5
+
6
+ export interface DecimalOptions {
7
+ precision?: number | null;
8
+ scale?: number;
9
+ }
10
+
11
+ /**
12
+ *
13
+ * @param options
14
+ * @decorator
15
+ * @objection
16
+ */
17
+ export function Decimal(options: DecimalOptions = {}) {
18
+ return useDecorators(
19
+ Property(),
20
+ ColumnOptions({
21
+ columnType: "number",
22
+ options: {
23
+ type: "decimal",
24
+ ...options
25
+ }
26
+ }),
27
+ StoreSet("objection", {
28
+ columnType: "number",
29
+ options: {
30
+ type: "decimal",
31
+ ...options
32
+ }
33
+ })
34
+ );
35
+ }
@@ -0,0 +1,24 @@
1
+ import {Entity} from "./entity.js";
2
+
3
+ describe("@Entity", () => {
4
+ it("should set metadata", () => {
5
+ @Entity("models")
6
+ class MyModel {}
7
+
8
+ expect((MyModel as any).jsonSchema).toEqual({
9
+ type: "object"
10
+ });
11
+ expect((MyModel as any).tableName).toEqual("models");
12
+ });
13
+
14
+ it("should throw metadata when the tableName is empty", () => {
15
+ let actualError: any;
16
+ try {
17
+ @Entity("")
18
+ class MyModel {}
19
+ } catch (er) {
20
+ actualError = er;
21
+ }
22
+ expect(actualError.message).toEqual("Please provide a table name");
23
+ });
24
+ });
@@ -0,0 +1,29 @@
1
+ import {Type} from "@tsed/core";
2
+ import {getJsonSchema} from "@tsed/schema";
3
+
4
+ import {defineStaticGetter} from "../utils/defineStaticGetter.js";
5
+ import {getJsonEntityRelationships} from "../utils/getJsonEntityRelationships.js";
6
+
7
+ /**
8
+ *
9
+ * @param tableName
10
+ * @decorator
11
+ * @objection
12
+ */
13
+ export function Entity(tableName: string): ClassDecorator {
14
+ if (!tableName) {
15
+ throw new Error("Please provide a table name");
16
+ }
17
+
18
+ return (target: any) => {
19
+ const originalRelationMappings = target["relationMappings"];
20
+ defineStaticGetter(target, "tableName", () => tableName);
21
+ defineStaticGetter(target, "jsonSchema", () => getJsonSchema(target));
22
+ defineStaticGetter(target, "relationMappings", () => originalRelationMappings || getJsonEntityRelationships(target));
23
+ return target;
24
+ };
25
+ }
26
+
27
+ export interface EntityMethods<T> extends Type<T> {
28
+ readonly tableName: string;
29
+ }
@@ -0,0 +1,60 @@
1
+ import {Model} from "objection";
2
+
3
+ import {Entity} from "./entity.js";
4
+ import {HasMany} from "./hasMany.js";
5
+ import {IdColumn} from "./idColumn.js";
6
+
7
+ describe("@HasMany", () => {
8
+ it("should set metadata", () => {
9
+ @Entity("pet")
10
+ class Pet extends Model {
11
+ @IdColumn()
12
+ id!: string;
13
+ userId?: string;
14
+ }
15
+
16
+ @Entity("user")
17
+ class User extends Model {
18
+ @IdColumn()
19
+ id!: string;
20
+ @HasMany(Pet)
21
+ pets?: Pet[];
22
+ }
23
+ expect(User.relationMappings).toEqual({
24
+ pets: {
25
+ relation: Model.HasManyRelation,
26
+ modelClass: Pet,
27
+ join: {
28
+ from: "user.id",
29
+ to: "pet.userId"
30
+ }
31
+ }
32
+ });
33
+ });
34
+ it("should set custom relationship path", () => {
35
+ @Entity("pet")
36
+ class Pet extends Model {
37
+ @IdColumn()
38
+ id!: string;
39
+ ownerId?: string;
40
+ }
41
+ @Entity("user")
42
+ class User extends Model {
43
+ @IdColumn()
44
+ id!: string;
45
+ userId!: string;
46
+ @HasMany(Pet, {from: "userId", to: "ownerId"})
47
+ pets?: Pet[];
48
+ }
49
+ expect(User.relationMappings).toEqual({
50
+ pets: {
51
+ relation: Model.HasManyRelation,
52
+ modelClass: Pet,
53
+ join: {
54
+ from: "user.userId",
55
+ to: "pet.ownerId"
56
+ }
57
+ }
58
+ });
59
+ });
60
+ });
@@ -0,0 +1,14 @@
1
+ import {Model, ModelClassSpecifier} from "objection";
2
+
3
+ import {RelationshipOptsWithThrough} from "../domain/RelationshipOpts.js";
4
+ import {RelatesTo} from "./relatesTo.js";
5
+
6
+ /**
7
+ *
8
+ * @param opts
9
+ * @decorator
10
+ * @objection
11
+ */
12
+ export function HasMany(type: ModelClassSpecifier, opts?: RelationshipOptsWithThrough): PropertyDecorator {
13
+ return RelatesTo(Model.HasManyRelation, {...opts, type});
14
+ }
@@ -0,0 +1,60 @@
1
+ import {Model} from "objection";
2
+
3
+ import {Entity} from "./entity.js";
4
+ import {HasOne} from "./hasOne.js";
5
+ import {IdColumn} from "./idColumn.js";
6
+
7
+ describe("@HasOne", () => {
8
+ it("should set metadata", () => {
9
+ @Entity("pet")
10
+ class Pet extends Model {
11
+ @IdColumn()
12
+ id!: string;
13
+ userId?: string;
14
+ }
15
+
16
+ @Entity("user")
17
+ class User extends Model {
18
+ @IdColumn()
19
+ id!: string;
20
+ @HasOne()
21
+ pet?: Pet;
22
+ }
23
+ expect(User.relationMappings).toEqual({
24
+ pet: {
25
+ relation: Model.HasOneRelation,
26
+ modelClass: Pet,
27
+ join: {
28
+ from: "user.id",
29
+ to: "pet.userId"
30
+ }
31
+ }
32
+ });
33
+ });
34
+ it("should set custom relationship path", () => {
35
+ @Entity("pet")
36
+ class Pet extends Model {
37
+ @IdColumn()
38
+ id!: string;
39
+ ownerId?: string;
40
+ }
41
+ @Entity("user")
42
+ class User extends Model {
43
+ @IdColumn()
44
+ id!: string;
45
+ userId!: string;
46
+ @HasOne({from: "userId", to: "ownerId"})
47
+ pet?: Pet;
48
+ }
49
+ expect(User.relationMappings).toEqual({
50
+ pet: {
51
+ relation: Model.HasOneRelation,
52
+ modelClass: Pet,
53
+ join: {
54
+ from: "user.userId",
55
+ to: "pet.ownerId"
56
+ }
57
+ }
58
+ });
59
+ });
60
+ });
@@ -0,0 +1,14 @@
1
+ import {Model} from "objection";
2
+
3
+ import {RelationshipOptsWithoutThrough} from "../domain/RelationshipOpts.js";
4
+ import {RelatesTo} from "./relatesTo.js";
5
+
6
+ /**
7
+ *
8
+ * @param opts
9
+ * @decorator
10
+ * @objection
11
+ */
12
+ export function HasOne(opts?: RelationshipOptsWithoutThrough): PropertyDecorator {
13
+ return RelatesTo(Model.HasOneRelation, opts);
14
+ }
@@ -0,0 +1,72 @@
1
+ import {Model} from "objection";
2
+
3
+ import {Entity} from "./entity.js";
4
+ import {HasOneThroughRelation} from "./hasOneThroughRelation.js";
5
+ import {IdColumn} from "./idColumn.js";
6
+
7
+ describe("@HasOneThroughRelation", () => {
8
+ it("should set metadata", () => {
9
+ @Entity("pet")
10
+ class Pet extends Model {
11
+ @IdColumn()
12
+ id!: string;
13
+ userId?: string;
14
+ }
15
+
16
+ @Entity("user")
17
+ class User extends Model {
18
+ @IdColumn()
19
+ id!: string;
20
+ @HasOneThroughRelation()
21
+ pet?: Pet;
22
+ }
23
+ expect(User.relationMappings).toEqual({
24
+ pet: {
25
+ relation: Model.HasOneThroughRelation,
26
+ modelClass: Pet,
27
+ join: {
28
+ from: "user.id",
29
+ through: {
30
+ from: "user_pet.userId",
31
+ to: "user_pet.petId"
32
+ },
33
+ to: "pet.id"
34
+ }
35
+ }
36
+ });
37
+ });
38
+ it("should set custom relationship path", () => {
39
+ @Entity("pet")
40
+ class Pet extends Model {
41
+ @IdColumn()
42
+ id!: string;
43
+ petId?: string;
44
+ }
45
+ @Entity("user")
46
+ class User extends Model {
47
+ @IdColumn()
48
+ id!: string;
49
+ userId!: string;
50
+ @HasOneThroughRelation({
51
+ from: "userId",
52
+ through: {from: "user_pet.ownerId", to: "user_pet.petId"},
53
+ to: "petId"
54
+ })
55
+ pet?: Pet;
56
+ }
57
+ expect(User.relationMappings).toEqual({
58
+ pet: {
59
+ relation: Model.HasOneThroughRelation,
60
+ modelClass: Pet,
61
+ join: {
62
+ from: "user.userId",
63
+ through: {
64
+ from: "user_pet.ownerId",
65
+ to: "user_pet.petId"
66
+ },
67
+ to: "pet.petId"
68
+ }
69
+ }
70
+ });
71
+ });
72
+ });
@@ -0,0 +1,14 @@
1
+ import {Model} from "objection";
2
+
3
+ import {RelationshipOptsWithoutThrough, RelationshipOptsWithThrough} from "../domain/RelationshipOpts.js";
4
+ import {RelatesTo} from "./relatesTo.js";
5
+
6
+ /**
7
+ *
8
+ * @param opts
9
+ * @decorator
10
+ * @objection
11
+ */
12
+ export function HasOneThroughRelation(opts?: RelationshipOptsWithThrough): PropertyDecorator {
13
+ return RelatesTo(Model.HasOneThroughRelation, opts);
14
+ }
@@ -0,0 +1,31 @@
1
+ import {Store} from "@tsed/core";
2
+ import {getJsonSchema} from "@tsed/schema";
3
+ import {Model} from "objection";
4
+
5
+ import {IdColumn} from "./idColumn.js";
6
+
7
+ describe("@IdColumn", () => {
8
+ it("should set metadata", () => {
9
+ class MyModel extends Model {
10
+ @IdColumn()
11
+ id: number;
12
+ }
13
+
14
+ expect(getJsonSchema(MyModel)).toEqual({
15
+ properties: {
16
+ id: {
17
+ type: "number"
18
+ }
19
+ },
20
+ type: "object"
21
+ });
22
+ expect(Store.from(MyModel, "id").get("objection")).toEqual({
23
+ columnType: "idColumn",
24
+ options: {
25
+ type: "increments"
26
+ }
27
+ });
28
+
29
+ expect(MyModel.idColumn).toEqual("id");
30
+ });
31
+ });
@@ -0,0 +1,26 @@
1
+ import {useDecorators} from "@tsed/core";
2
+ import {Property} from "@tsed/schema";
3
+
4
+ import {defineStaticGetter} from "../utils/defineStaticGetter.js";
5
+ import {ColumnOptions} from "./columnOptions.js";
6
+
7
+ /**
8
+ *
9
+ * @param type
10
+ * @decorator
11
+ * @objection
12
+ */
13
+ export function IdColumn(type: "uuid" | "increments" | "bigIncrements" = "increments"): PropertyDecorator {
14
+ return useDecorators(
15
+ Property(),
16
+ ColumnOptions({
17
+ columnType: "idColumn",
18
+ options: {
19
+ type
20
+ }
21
+ }),
22
+ (target: any, propertyKey: string) => {
23
+ defineStaticGetter(target, "idColumn", () => propertyKey);
24
+ }
25
+ );
26
+ }
@@ -0,0 +1,72 @@
1
+ import {Model} from "objection";
2
+
3
+ import {Entity} from "./entity.js";
4
+ import {IdColumn} from "./idColumn.js";
5
+ import {ManyToMany} from "./manyToMany.js";
6
+
7
+ describe("@ManyToMany", () => {
8
+ it("should set metadata", () => {
9
+ @Entity("pet")
10
+ class Pet extends Model {
11
+ @IdColumn()
12
+ id!: string;
13
+ userId?: string;
14
+ }
15
+
16
+ @Entity("user")
17
+ class User extends Model {
18
+ @IdColumn()
19
+ id!: string;
20
+ @ManyToMany(Pet)
21
+ pets?: Pet[];
22
+ }
23
+ expect(User.relationMappings).toEqual({
24
+ pets: {
25
+ relation: Model.ManyToManyRelation,
26
+ modelClass: Pet,
27
+ join: {
28
+ from: "user.id",
29
+ through: {
30
+ from: "user_pet.userId",
31
+ to: "user_pet.petId"
32
+ },
33
+ to: "pet.id"
34
+ }
35
+ }
36
+ });
37
+ });
38
+ it("should set custom relationship path", () => {
39
+ @Entity("pet")
40
+ class Pet extends Model {
41
+ @IdColumn()
42
+ id!: string;
43
+ petId?: string;
44
+ }
45
+ @Entity("user")
46
+ class User extends Model {
47
+ @IdColumn()
48
+ id!: string;
49
+ userId!: string;
50
+ @ManyToMany(Pet, {
51
+ from: "userId",
52
+ through: {from: "user_pet.ownerId", to: "user_pet.petId"},
53
+ to: "petId"
54
+ })
55
+ pets?: Pet[];
56
+ }
57
+ expect(User.relationMappings).toEqual({
58
+ pets: {
59
+ relation: Model.ManyToManyRelation,
60
+ modelClass: Pet,
61
+ join: {
62
+ from: "user.userId",
63
+ through: {
64
+ from: "user_pet.ownerId",
65
+ to: "user_pet.petId"
66
+ },
67
+ to: "pet.petId"
68
+ }
69
+ }
70
+ });
71
+ });
72
+ });
@@ -0,0 +1,14 @@
1
+ import {Model, ModelClassSpecifier} from "objection";
2
+
3
+ import {RelationshipOptsWithThrough} from "../domain/RelationshipOpts.js";
4
+ import {RelatesTo} from "./relatesTo.js";
5
+
6
+ /**
7
+ *
8
+ * @param opts
9
+ * @decorator
10
+ * @objection
11
+ */
12
+ export function ManyToMany(type: ModelClassSpecifier, opts?: RelationshipOptsWithThrough): PropertyDecorator {
13
+ return RelatesTo(Model.ManyToManyRelation, {...opts, type});
14
+ }
@@ -0,0 +1,34 @@
1
+ import {Model} from "objection";
2
+
3
+ import {Entity} from "./entity.js";
4
+ import {IdColumn} from "./idColumn.js";
5
+ import {RelatesTo} from "./relatesTo.js";
6
+
7
+ describe("@RelatesTo", () => {
8
+ it("should set metadata", () => {
9
+ @Entity("user")
10
+ class User extends Model {
11
+ @IdColumn()
12
+ id!: string;
13
+ }
14
+
15
+ @Entity("movie")
16
+ class Movie extends Model {
17
+ @IdColumn()
18
+ id!: string;
19
+ userId!: string;
20
+ @RelatesTo(Model.BelongsToOneRelation)
21
+ user?: User;
22
+ }
23
+ expect(Movie.relationMappings).toEqual({
24
+ user: {
25
+ relation: Model.BelongsToOneRelation,
26
+ modelClass: User,
27
+ join: {
28
+ from: "movie.userId",
29
+ to: "user.id"
30
+ }
31
+ }
32
+ });
33
+ });
34
+ });
@@ -0,0 +1,26 @@
1
+ import {StoreFn, useDecorators} from "@tsed/core";
2
+ import {CollectionOf, Property} from "@tsed/schema";
3
+ import {RelationType} from "objection";
4
+
5
+ import {isModelClassFactory, RelationshipOpts} from "../domain/RelationshipOpts.js";
6
+ import {createRelationshipMapping} from "../utils/createRelationshipMapping.js";
7
+ import {OBJECTION_RELATIONSHIP_KEY} from "../utils/getJsonEntityRelationships.js";
8
+
9
+ /**
10
+ *
11
+ * @param relation
12
+ * @param opts
13
+ * @decorator
14
+ * @objection
15
+ */
16
+ export function RelatesTo(relation: RelationType, opts?: RelationshipOpts): PropertyDecorator {
17
+ return useDecorators(
18
+ opts?.type ? CollectionOf(opts.type) : Property(),
19
+ StoreFn((store, params) => {
20
+ if (opts && isModelClassFactory(opts.type)) {
21
+ opts.type = opts.type();
22
+ }
23
+ store.set(OBJECTION_RELATIONSHIP_KEY, createRelationshipMapping(params, relation, opts));
24
+ })
25
+ );
26
+ }
@@ -0,0 +1,4 @@
1
+ export interface ColumnOpts {
2
+ columnType: "idColumn" | "string" | "number" | "boolean";
3
+ options: any;
4
+ }
@@ -0,0 +1,14 @@
1
+ import {isFunction, isString} from "@tsed/core";
2
+ import {ModelClassFactory, ModelClassSpecifier, RelationJoin, RelationMapping} from "objection";
3
+
4
+ export type RelationshipOptsWithThrough = Partial<RelationJoin> & Omit<RelationMapping<any>, "relation" | "join" | "modelClass">;
5
+
6
+ export type RelationshipOptsWithoutThrough = Omit<RelationshipOptsWithThrough, "through">;
7
+
8
+ export type RelationshipOpts = (RelationshipOptsWithThrough | RelationshipOptsWithoutThrough) & {type?: ModelClassSpecifier};
9
+
10
+ export const isRelationshipOptsWithThrough = (opts?: RelationshipOpts): opts is RelationshipOptsWithThrough =>
11
+ opts !== undefined && (<RelationshipOptsWithThrough>opts).through !== undefined;
12
+
13
+ export const isModelClassFactory = (type?: ModelClassSpecifier): type is ModelClassFactory =>
14
+ type !== undefined && !isString(type) && isFunction(type) && !("prototype" in type);
@@ -0,0 +1,9 @@
1
+ import {Knex} from "knex";
2
+
3
+ declare global {
4
+ namespace TsED {
5
+ interface Configuration {
6
+ knex: Knex.Config;
7
+ }
8
+ }
9
+ }
package/src/index.ts ADDED
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @file Automatically generated by @tsed/barrels.
3
+ */
4
+ export * from "./components/createBooleanColumn.js";
5
+ export * from "./components/createIdColumn.js";
6
+ export * from "./components/createNumberColumn.js";
7
+ export * from "./components/createStringColumn.js";
8
+ export * from "./decorators/belongsToOne.js";
9
+ export * from "./decorators/columnOptions.js";
10
+ export * from "./decorators/decimal.js";
11
+ export * from "./decorators/entity.js";
12
+ export * from "./decorators/hasMany.js";
13
+ export * from "./decorators/hasOne.js";
14
+ export * from "./decorators/hasOneThroughRelation.js";
15
+ export * from "./decorators/idColumn.js";
16
+ export * from "./decorators/manyToMany.js";
17
+ export * from "./decorators/relatesTo.js";
18
+ export * from "./domain/ColumnOpts.js";
19
+ export * from "./domain/interfaces.js";
20
+ export * from "./domain/RelationshipOpts.js";
21
+ export * from "./services/ColumnTypesContainer.js";
22
+ export * from "./services/ObjectionConnection.js";
23
+ export * from "./utils/connect.js";
24
+ export * from "./utils/createColumns.js";
25
+ export * from "./utils/createJoinKeys.js";
26
+ export * from "./utils/createRelationshipMapping.js";
27
+ export * from "./utils/defineStaticGetter.js";
28
+ export * from "./utils/getColumnCtx.js";
29
+ export * from "./utils/getJsonEntityRelationships.js";
@@ -0,0 +1,8 @@
1
+ import type {Knex} from "knex";
2
+
3
+ import {ColumnCtx} from "../utils/getColumnCtx.js";
4
+
5
+ /**
6
+ * @ignore
7
+ */
8
+ export const ColumnTypesContainer: Map<string, (table: Knex.TableBuilder, ctx: ColumnCtx) => void> = new Map();
@@ -0,0 +1,26 @@
1
+ import {Configuration, registerProvider} from "@tsed/di";
2
+ import type {Knex} from "knex";
3
+
4
+ import {createConnection} from "../utils/connect.js";
5
+
6
+ export const OBJECTION_CONNECTION = Symbol.for("DEFAULT_CONNECTION");
7
+ export type OBJECTION_CONNECTION = Knex<any, unknown[]>;
8
+
9
+ registerProvider({
10
+ token: OBJECTION_CONNECTION,
11
+ deps: [Configuration],
12
+ useAsyncFactory(configuration: Configuration) {
13
+ const connectionOptions = configuration.get<Knex.Config>("knex")!;
14
+
15
+ if (connectionOptions) {
16
+ try {
17
+ return createConnection(connectionOptions);
18
+ } catch (er) {
19
+ // istanbul ignore next
20
+ console.error(er);
21
+ }
22
+ }
23
+
24
+ return Promise.resolve({});
25
+ }
26
+ });
@@ -0,0 +1,9 @@
1
+ import knex, {Knex} from "knex";
2
+ import {Model} from "objection";
3
+
4
+ export function createConnection(connectionOptions: Knex.Config): Knex {
5
+ const connection = knex(connectionOptions);
6
+ Model.knex(connection);
7
+
8
+ return connection;
9
+ }
@@ -0,0 +1,79 @@
1
+ import {catchError} from "@tsed/core";
2
+ import {MaxLength, Property} from "@tsed/schema";
3
+
4
+ import {createTableStub} from "../../test/helpers/knex/table.js";
5
+ import {Decimal} from "../decorators/decimal.js";
6
+ import {Entity} from "../decorators/entity.js";
7
+ import {IdColumn} from "../decorators/idColumn.js";
8
+ import {createColumns} from "./createColumns.js";
9
+
10
+ describe("createTable", () => {
11
+ it("should create table from a given class (columns doesn't exists)", () => {
12
+ @Entity("users")
13
+ class User {
14
+ @IdColumn()
15
+ id: number;
16
+
17
+ @Property()
18
+ @MaxLength(200)
19
+ name: string;
20
+
21
+ @Property()
22
+ age: number;
23
+
24
+ @Decimal({scale: 1, precision: 12})
25
+ score: number;
26
+
27
+ @Property()
28
+ active: boolean;
29
+ }
30
+
31
+ const table = createTableStub();
32
+
33
+ createColumns(table, User);
34
+
35
+ expect(table.increments).toHaveBeenCalledWith("id");
36
+ expect(table.primary).toHaveBeenCalledWith();
37
+ expect(table.string).toHaveBeenCalledWith("name", 200);
38
+ expect(table.decimal).toHaveBeenCalledWith("age", undefined, undefined);
39
+ expect(table.decimal).toHaveBeenCalledWith("score", 12, 1);
40
+ expect(table.boolean).toHaveBeenCalledWith("active");
41
+ });
42
+ it("should create table from a given class (columns exists)", () => {
43
+ @Entity("users")
44
+ class User {
45
+ @IdColumn()
46
+ id: number;
47
+
48
+ @Property()
49
+ @MaxLength(200)
50
+ name: string;
51
+
52
+ @Property()
53
+ age: number;
54
+
55
+ @Decimal({scale: 1, precision: 12})
56
+ score: number;
57
+
58
+ @Property()
59
+ active: boolean;
60
+ }
61
+
62
+ const table = createTableStub();
63
+
64
+ createColumns(table, User);
65
+ });
66
+
67
+ it("should throw error when type isn't supported", () => {
68
+ @Entity("users")
69
+ class User {
70
+ @Property()
71
+ date: any;
72
+ }
73
+
74
+ const table = createTableStub();
75
+ const error = catchError(() => createColumns(table, User));
76
+
77
+ expect(error).toBe("Column type object isn't supported. Add function to ColumnTypesContainer to map the column with Knex");
78
+ });
79
+ });
@@ -0,0 +1,39 @@
1
+ import "../components/createBooleanColumn.js";
2
+ import "../components/createIdColumn.js";
3
+ import "../components/createNumberColumn.js";
4
+ import "../components/createStringColumn.js";
5
+
6
+ import {getProperties, JsonEntityStore} from "@tsed/schema";
7
+ import {Knex} from "knex";
8
+
9
+ import {ColumnTypesContainer} from "../services/ColumnTypesContainer.js";
10
+ import {getColumnCtx} from "./getColumnCtx.js";
11
+
12
+ /**
13
+ * @ignore
14
+ */
15
+ export function createColumn(table: Knex.TableBuilder, entity: JsonEntityStore) {
16
+ const ctx = getColumnCtx(entity);
17
+
18
+ if (!ColumnTypesContainer.has(ctx.columnType)) {
19
+ throw `Column type ${ctx.columnType} isn't supported. Add function to ColumnTypesContainer to map the column with Knex`;
20
+ }
21
+
22
+ ColumnTypesContainer.get(ctx.columnType)!(table, ctx);
23
+ }
24
+
25
+ /**
26
+ * @ignore
27
+ */
28
+ export function getColumns(model: any) {
29
+ return [...getProperties(model, {withIgnoredProps: true, objection: true, groups: false}).values()];
30
+ }
31
+
32
+ /**
33
+ * @ignore
34
+ */
35
+ export function createColumns(table: Knex.TableBuilder, model: any) {
36
+ getColumns(model).map((entity) => {
37
+ createColumn(table, entity);
38
+ });
39
+ }
@@ -0,0 +1,38 @@
1
+ import {Metadata} from "@tsed/core";
2
+ import {Model, RelationType} from "objection";
3
+
4
+ import {isRelationshipOptsWithThrough, RelationshipOpts} from "../domain/RelationshipOpts.js";
5
+
6
+ /**
7
+ * @ignore
8
+ */
9
+ export function createJoinKeys(targetClass: any, targetSuper: any, propertyKey: string, relation: RelationType, opts?: RelationshipOpts) {
10
+ const FOREIGN_MODEL = opts?.type || Metadata.getType(targetSuper, propertyKey);
11
+ const SOURCE_MODEL_KEY = `${targetClass.tableName}.${opts?.from || targetClass.idColumn}`;
12
+ const FOREIGN_MODEL_KEY = `${FOREIGN_MODEL.tableName}.${opts?.to || FOREIGN_MODEL.idColumn}`;
13
+
14
+ switch (relation) {
15
+ case Model.HasManyRelation:
16
+ case Model.HasOneRelation:
17
+ return {
18
+ from: SOURCE_MODEL_KEY,
19
+ to: `${FOREIGN_MODEL.tableName}.${opts?.to || targetClass.tableName + "Id"}`
20
+ };
21
+ case Model.ManyToManyRelation:
22
+ case Model.HasOneThroughRelation:
23
+ return {
24
+ from: SOURCE_MODEL_KEY,
25
+ through: (isRelationshipOptsWithThrough(opts) && opts.through) || {
26
+ from: `${targetClass.tableName}_${FOREIGN_MODEL.tableName}.${targetClass.tableName}Id`,
27
+ to: `${targetClass.tableName}_${FOREIGN_MODEL.tableName}.${FOREIGN_MODEL.tableName}Id`
28
+ },
29
+ to: FOREIGN_MODEL_KEY
30
+ };
31
+ case Model.BelongsToOneRelation:
32
+ default:
33
+ return {
34
+ from: `${targetClass.tableName}.${opts?.from || propertyKey + "Id"}`,
35
+ to: FOREIGN_MODEL_KEY
36
+ };
37
+ }
38
+ }
@@ -0,0 +1,21 @@
1
+ import {DecoratorParameters, getClass, Metadata} from "@tsed/core";
2
+ import {RelationType} from "objection";
3
+
4
+ import {RelationshipOpts} from "../domain/RelationshipOpts.js";
5
+ import {createJoinKeys} from "./createJoinKeys.js";
6
+
7
+ /**
8
+ * @ignore
9
+ */
10
+ export function createRelationshipMapping([target, propertyKey]: DecoratorParameters, relation: RelationType, opts?: RelationshipOpts) {
11
+ return (targetClass: any) => ({
12
+ [propertyKey]: {
13
+ relation,
14
+ modelClass: opts?.type || Metadata.getType(target, propertyKey),
15
+ join: createJoinKeys(targetClass, target, String(propertyKey), relation, opts),
16
+ modify: opts?.modify,
17
+ filter: opts?.filter,
18
+ beforeInsert: opts?.beforeInsert
19
+ }
20
+ });
21
+ }
@@ -0,0 +1,10 @@
1
+ import {classOf} from "@tsed/core";
2
+
3
+ /**
4
+ * @ignore
5
+ */
6
+ export function defineStaticGetter(target: any, propertyKey: string, fn: any) {
7
+ return Object.defineProperty(classOf(target), propertyKey, {
8
+ get: fn
9
+ });
10
+ }
@@ -0,0 +1,21 @@
1
+ import {JsonEntityStore} from "@tsed/schema";
2
+
3
+ import {ColumnOpts} from "../domain/ColumnOpts.js";
4
+
5
+ /**
6
+ * @ignore
7
+ */
8
+ export interface ColumnCtx extends ColumnOpts {
9
+ entity: JsonEntityStore;
10
+ schema: any;
11
+ }
12
+
13
+ /**
14
+ * @ignore
15
+ */
16
+ export function getColumnCtx(entity: JsonEntityStore): ColumnCtx {
17
+ const schema = entity.schema.toJSON();
18
+ const {columnType = schema.type, options = {}} = entity.store.get<Partial<ColumnOpts>>("objection", {});
19
+
20
+ return {entity, columnType, options, schema};
21
+ }
@@ -0,0 +1,16 @@
1
+ import {getColumns} from "./createColumns.js";
2
+
3
+ /**
4
+ * @ignore
5
+ */
6
+ export const OBJECTION_RELATIONSHIP_KEY = "objectionRelationship";
7
+
8
+ /**
9
+ * @ignore
10
+ */
11
+ export function getJsonEntityRelationships(target: any) {
12
+ return getColumns(target)
13
+ .filter((col) => col.store.get(OBJECTION_RELATIONSHIP_KEY))
14
+ .map((col) => col.store.get(OBJECTION_RELATIONSHIP_KEY)(target))
15
+ .reduce((dict, col) => ({...dict, ...col}), {});
16
+ }