orchid-orm 0.0.1
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/.env.example +1 -0
- package/.rush/temp/shrinkwrap-deps.json +327 -0
- package/README.md +5 -0
- package/dist/index.d.ts +228 -0
- package/dist/index.esm.js +1116 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +1122 -0
- package/dist/index.js.map +1 -0
- package/jest-setup.ts +7 -0
- package/package.json +58 -0
- package/rollup.config.js +3 -0
- package/src/index.ts +3 -0
- package/src/model.test.ts +92 -0
- package/src/model.ts +169 -0
- package/src/orm.test.ts +73 -0
- package/src/orm.ts +74 -0
- package/src/relations/belongsTo.test.ts +890 -0
- package/src/relations/belongsTo.ts +247 -0
- package/src/relations/hasAndBelongsToMany.test.ts +1122 -0
- package/src/relations/hasAndBelongsToMany.ts +398 -0
- package/src/relations/hasMany.test.ts +2003 -0
- package/src/relations/hasMany.ts +332 -0
- package/src/relations/hasOne.test.ts +1219 -0
- package/src/relations/hasOne.ts +278 -0
- package/src/relations/relations.test.ts +37 -0
- package/src/relations/relations.ts +316 -0
- package/src/relations/utils.ts +12 -0
- package/src/repo.test.ts +200 -0
- package/src/repo.ts +119 -0
- package/src/test-utils/test-db.ts +32 -0
- package/src/test-utils/test-models.ts +194 -0
- package/src/test-utils/test-utils.ts +69 -0
- package/src/transaction.test.ts +45 -0
- package/src/transaction.ts +27 -0
- package/tsconfig.json +13 -0
package/src/repo.test.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { orchidORM } from './orm';
|
|
2
|
+
import { pgConfig } from './test-utils/test-db';
|
|
3
|
+
import { createModel } from './model';
|
|
4
|
+
import { assertType, expectSql } from './test-utils/test-utils';
|
|
5
|
+
import { columnTypes, QueryReturnType } from 'pqb';
|
|
6
|
+
import { createRepo } from './repo';
|
|
7
|
+
|
|
8
|
+
const Model = createModel({ columnTypes });
|
|
9
|
+
|
|
10
|
+
class SomeModel extends Model {
|
|
11
|
+
table = 'someTable';
|
|
12
|
+
columns = this.setColumns((t) => ({
|
|
13
|
+
id: t.serial().primaryKey(),
|
|
14
|
+
name: t.text(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
relations = {
|
|
18
|
+
otherModel: this.hasMany(() => OtherModel, {
|
|
19
|
+
primaryKey: 'id',
|
|
20
|
+
foreignKey: 'someId',
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class OtherModel extends Model {
|
|
26
|
+
table = 'otherTable';
|
|
27
|
+
columns = this.setColumns((t) => ({
|
|
28
|
+
id: t.serial().primaryKey(),
|
|
29
|
+
someId: t.integer().foreignKey(() => SomeModel, 'id'),
|
|
30
|
+
anotherId: t.integer().foreignKey(() => AnotherModel, 'id'),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
relations = {
|
|
34
|
+
someModel: this.belongsTo(() => SomeModel, {
|
|
35
|
+
primaryKey: 'id',
|
|
36
|
+
foreignKey: 'someId',
|
|
37
|
+
}),
|
|
38
|
+
anotherModel: this.belongsTo(() => AnotherModel, {
|
|
39
|
+
primaryKey: 'id',
|
|
40
|
+
foreignKey: 'anotherId',
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class AnotherModel extends Model {
|
|
46
|
+
table = 'anotherModel';
|
|
47
|
+
columns = this.setColumns((t) => ({
|
|
48
|
+
id: t.serial().primaryKey(),
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const db = orchidORM(pgConfig, {
|
|
53
|
+
someModel: SomeModel,
|
|
54
|
+
otherModel: OtherModel,
|
|
55
|
+
anotherModel: AnotherModel,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('createRepo', () => {
|
|
59
|
+
describe('queryMethods', () => {
|
|
60
|
+
const repo = createRepo(db.someModel, {
|
|
61
|
+
queryMethods: {
|
|
62
|
+
one(q) {
|
|
63
|
+
return q.select('id');
|
|
64
|
+
},
|
|
65
|
+
two(q) {
|
|
66
|
+
return q.select('name');
|
|
67
|
+
},
|
|
68
|
+
three(q, id: number) {
|
|
69
|
+
return q.where({ id });
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should accept user defined methods and allow to use them on the model with chaining', async () => {
|
|
75
|
+
const q = repo.one().two().three(123).take();
|
|
76
|
+
|
|
77
|
+
assertType<Awaited<typeof q>, { id: number; name: string }>();
|
|
78
|
+
|
|
79
|
+
expectSql(
|
|
80
|
+
q.toSql(),
|
|
81
|
+
`
|
|
82
|
+
SELECT "someTable"."id", "someTable"."name"
|
|
83
|
+
FROM "someTable"
|
|
84
|
+
WHERE "someTable"."id" = $1
|
|
85
|
+
LIMIT $2
|
|
86
|
+
`,
|
|
87
|
+
[123, 1],
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should have custom methods on relation queries inside of select', async () => {
|
|
92
|
+
const q = db.otherModel.select('id', {
|
|
93
|
+
someModel: (q) => repo(q.someModel).one().two().three(123),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
assertType<
|
|
97
|
+
Awaited<typeof q>,
|
|
98
|
+
{ id: number; someModel: { id: number; name: string } | null }[]
|
|
99
|
+
>();
|
|
100
|
+
|
|
101
|
+
expectSql(
|
|
102
|
+
q.toSql(),
|
|
103
|
+
`
|
|
104
|
+
SELECT
|
|
105
|
+
"otherTable"."id",
|
|
106
|
+
(
|
|
107
|
+
SELECT row_to_json("t".*)
|
|
108
|
+
FROM (
|
|
109
|
+
SELECT "someModel"."id", "someModel"."name"
|
|
110
|
+
FROM "someTable" AS "someModel"
|
|
111
|
+
WHERE "someModel"."id" = $1
|
|
112
|
+
AND "someModel"."id" = "otherTable"."someId"
|
|
113
|
+
LIMIT $2
|
|
114
|
+
) AS "t"
|
|
115
|
+
) AS "someModel"
|
|
116
|
+
FROM "otherTable"
|
|
117
|
+
`,
|
|
118
|
+
[123, 1],
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('queryOneMethods', () => {
|
|
124
|
+
const repo = createRepo(db.someModel, {
|
|
125
|
+
queryOneMethods: {
|
|
126
|
+
one(q) {
|
|
127
|
+
const type: Exclude<QueryReturnType, 'all'> = q.returnType;
|
|
128
|
+
return type;
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should define methods which are available only after .take, .find, or similar', () => {
|
|
134
|
+
// @ts-expect-error should prevent using method on query which returns multiple
|
|
135
|
+
repo.one();
|
|
136
|
+
|
|
137
|
+
repo.take().one();
|
|
138
|
+
repo.find(1).one();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('queryWithWhereMethods', () => {
|
|
143
|
+
const repo = createRepo(db.someModel, {
|
|
144
|
+
queryWithWhereMethods: {
|
|
145
|
+
one(q) {
|
|
146
|
+
const hasWhere: true = q.hasWhere;
|
|
147
|
+
return hasWhere;
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should define methods which are available only after .where, .find, or similar', () => {
|
|
153
|
+
// @ts-expect-error should prevent using method on query without where conditions
|
|
154
|
+
repo.one();
|
|
155
|
+
// @ts-expect-error should prevent using method on query without where conditions
|
|
156
|
+
repo.take().one();
|
|
157
|
+
|
|
158
|
+
repo.where().one();
|
|
159
|
+
repo.find(1).one();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('queryOneWithWhere', () => {
|
|
164
|
+
const repo = createRepo(db.someModel, {
|
|
165
|
+
queryOneWithWhereMethods: {
|
|
166
|
+
one(q) {
|
|
167
|
+
const type: Exclude<QueryReturnType, 'all'> = q.returnType;
|
|
168
|
+
const hasWhere: true = q.hasWhere;
|
|
169
|
+
return [type, hasWhere];
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should define methods which are available only after .where, .find, or similar', () => {
|
|
175
|
+
// @ts-expect-error should prevent using method on query without where conditions
|
|
176
|
+
repo.one();
|
|
177
|
+
// @ts-expect-error should prevent using method on query without where conditions
|
|
178
|
+
repo.take().one();
|
|
179
|
+
|
|
180
|
+
// @ts-expect-error should prevent using method on query which returns multiple
|
|
181
|
+
repo.where().one();
|
|
182
|
+
|
|
183
|
+
repo.find(1).one();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('methods', () => {
|
|
188
|
+
const repo = createRepo(db.someModel, {
|
|
189
|
+
methods: {
|
|
190
|
+
one(a: number, b: string) {
|
|
191
|
+
return a + b;
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should assign methods as is to the repo', () => {
|
|
197
|
+
expect(repo.take().one(1, '2')).toBe('12');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
package/src/repo.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EmptyObject,
|
|
3
|
+
getClonedQueryData,
|
|
4
|
+
MergeQuery,
|
|
5
|
+
Query,
|
|
6
|
+
QueryReturnType,
|
|
7
|
+
SetQueryReturns,
|
|
8
|
+
WhereResult,
|
|
9
|
+
} from 'pqb';
|
|
10
|
+
|
|
11
|
+
export type QueryMethods<T extends Query> = Record<
|
|
12
|
+
string,
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
(q: T, ...args: any[]) => any
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
type QueryOne<T extends Query> = SetQueryReturns<
|
|
18
|
+
T,
|
|
19
|
+
Exclude<QueryReturnType, 'all'>
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
export type MethodsBase<T extends Query> = {
|
|
23
|
+
queryMethods?: QueryMethods<T>;
|
|
24
|
+
queryOneMethods?: QueryMethods<QueryOne<T>>;
|
|
25
|
+
queryWithWhereMethods?: QueryMethods<WhereResult<T>>;
|
|
26
|
+
queryOneWithWhereMethods?: QueryMethods<QueryOne<WhereResult<T>>>;
|
|
27
|
+
methods?: Record<string, unknown>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type MapQueryMethods<
|
|
31
|
+
T extends Query,
|
|
32
|
+
BaseQuery extends Query,
|
|
33
|
+
Methods,
|
|
34
|
+
> = Methods extends QueryMethods<T>
|
|
35
|
+
? {
|
|
36
|
+
[K in keyof Methods]: Methods[K] extends (
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
+
q: any,
|
|
39
|
+
...args: infer Args
|
|
40
|
+
) => // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
41
|
+
infer Result
|
|
42
|
+
? <T extends BaseQuery>(
|
|
43
|
+
this: T,
|
|
44
|
+
...args: Args
|
|
45
|
+
) => Result extends Query ? MergeQuery<T, Result> : Result
|
|
46
|
+
: never;
|
|
47
|
+
}
|
|
48
|
+
: EmptyObject;
|
|
49
|
+
|
|
50
|
+
export type MapMethods<
|
|
51
|
+
T extends Query,
|
|
52
|
+
Methods extends MethodsBase<T>,
|
|
53
|
+
> = MapQueryMethods<T, Query, Methods['queryMethods']> &
|
|
54
|
+
MapQueryMethods<QueryOne<T>, QueryOne<Query>, Methods['queryOneMethods']> &
|
|
55
|
+
MapQueryMethods<
|
|
56
|
+
WhereResult<T>,
|
|
57
|
+
WhereResult<Query>,
|
|
58
|
+
Methods['queryWithWhereMethods']
|
|
59
|
+
> &
|
|
60
|
+
MapQueryMethods<
|
|
61
|
+
QueryOne<WhereResult<T>>,
|
|
62
|
+
QueryOne<WhereResult<Query>>,
|
|
63
|
+
Methods['queryOneWithWhereMethods']
|
|
64
|
+
> &
|
|
65
|
+
(Methods['methods'] extends Record<string, unknown>
|
|
66
|
+
? Methods['methods']
|
|
67
|
+
: EmptyObject);
|
|
68
|
+
|
|
69
|
+
export type Repo<
|
|
70
|
+
T extends Query,
|
|
71
|
+
Methods extends MethodsBase<T>,
|
|
72
|
+
Mapped = MapMethods<T, Methods>,
|
|
73
|
+
> = (<Q extends { table: T['table']; shape: T['shape'] }>(q: Q) => Q & Mapped) &
|
|
74
|
+
T &
|
|
75
|
+
Mapped;
|
|
76
|
+
|
|
77
|
+
export const createRepo = <T extends Query, Methods extends MethodsBase<T>>(
|
|
78
|
+
model: T,
|
|
79
|
+
methods: Methods,
|
|
80
|
+
): Repo<T, Methods> => {
|
|
81
|
+
const queryMethods = {
|
|
82
|
+
...methods.queryMethods,
|
|
83
|
+
...methods.queryOneMethods,
|
|
84
|
+
...methods.queryWithWhereMethods,
|
|
85
|
+
...methods.queryOneWithWhereMethods,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const plainMethods = methods.methods;
|
|
89
|
+
|
|
90
|
+
const repo = (q: Query) => {
|
|
91
|
+
const proto = Object.create(q.__model);
|
|
92
|
+
proto.__model = proto;
|
|
93
|
+
const result = Object.create(proto);
|
|
94
|
+
result.query = getClonedQueryData(q.query);
|
|
95
|
+
|
|
96
|
+
if (plainMethods) {
|
|
97
|
+
Object.assign(proto.__model, plainMethods);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const key in queryMethods) {
|
|
101
|
+
const method = queryMethods[key] as (...args: unknown[]) => unknown;
|
|
102
|
+
(proto.__model as unknown as Record<string, unknown>)[key] = function (
|
|
103
|
+
...args: unknown[]
|
|
104
|
+
) {
|
|
105
|
+
return method(this, ...args);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return result;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const q = repo(model);
|
|
113
|
+
|
|
114
|
+
return new Proxy(repo, {
|
|
115
|
+
get(_, key) {
|
|
116
|
+
return q[key];
|
|
117
|
+
},
|
|
118
|
+
}) as unknown as Repo<T, Methods>;
|
|
119
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { orchidORM } from '../orm';
|
|
2
|
+
import {
|
|
3
|
+
ChatModel,
|
|
4
|
+
MessageModel,
|
|
5
|
+
PostModel,
|
|
6
|
+
PostTagModel,
|
|
7
|
+
ProfileModel,
|
|
8
|
+
TagModel,
|
|
9
|
+
UserModel,
|
|
10
|
+
} from './test-models';
|
|
11
|
+
|
|
12
|
+
export const pgConfig = {
|
|
13
|
+
connectionString: process.env.DATABASE_URL,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const db = orchidORM(
|
|
17
|
+
{
|
|
18
|
+
...pgConfig,
|
|
19
|
+
log: false,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
user: UserModel,
|
|
23
|
+
profile: ProfileModel,
|
|
24
|
+
chat: ChatModel,
|
|
25
|
+
message: MessageModel,
|
|
26
|
+
post: PostModel,
|
|
27
|
+
postTag: PostTagModel,
|
|
28
|
+
tag: TagModel,
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export const adapter = db.$adapter;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { createModel } from '../model';
|
|
2
|
+
import { columnTypes } from 'pqb';
|
|
3
|
+
import { modelToZod } from 'orchid-orm-schema-to-zod';
|
|
4
|
+
|
|
5
|
+
export const Model = createModel({
|
|
6
|
+
columnTypes: {
|
|
7
|
+
...columnTypes,
|
|
8
|
+
timestamp() {
|
|
9
|
+
return columnTypes.timestamp().parse((input) => new Date(input));
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export type User = UserModel['columns']['type'];
|
|
15
|
+
export class UserModel extends Model {
|
|
16
|
+
table = 'user';
|
|
17
|
+
columns = this.setColumns((t) => ({
|
|
18
|
+
id: t.serial().primaryKey(),
|
|
19
|
+
name: t.text(),
|
|
20
|
+
password: t.text(),
|
|
21
|
+
picture: t.text().nullable(),
|
|
22
|
+
data: t
|
|
23
|
+
.json((j) =>
|
|
24
|
+
j.object({
|
|
25
|
+
name: j.string(),
|
|
26
|
+
tags: j.string().array(),
|
|
27
|
+
}),
|
|
28
|
+
)
|
|
29
|
+
.nullable(),
|
|
30
|
+
age: t.integer().nullable(),
|
|
31
|
+
active: t.boolean().nullable(),
|
|
32
|
+
...t.timestamps(),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
relations = {
|
|
36
|
+
profile: this.hasOne(() => ProfileModel, {
|
|
37
|
+
required: true,
|
|
38
|
+
primaryKey: 'id',
|
|
39
|
+
foreignKey: 'userId',
|
|
40
|
+
}),
|
|
41
|
+
|
|
42
|
+
messages: this.hasMany(() => MessageModel, {
|
|
43
|
+
primaryKey: 'id',
|
|
44
|
+
foreignKey: 'authorId',
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
chats: this.hasAndBelongsToMany(() => ChatModel, {
|
|
48
|
+
primaryKey: 'id',
|
|
49
|
+
foreignKey: 'userId',
|
|
50
|
+
associationPrimaryKey: 'id',
|
|
51
|
+
associationForeignKey: 'chatId',
|
|
52
|
+
joinTable: 'chatUser',
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export const UserSchema = modelToZod(UserModel);
|
|
57
|
+
|
|
58
|
+
export type Profile = ProfileModel['columns']['type'];
|
|
59
|
+
export class ProfileModel extends Model {
|
|
60
|
+
table = 'profile';
|
|
61
|
+
columns = this.setColumns((t) => ({
|
|
62
|
+
id: t.serial().primaryKey(),
|
|
63
|
+
userId: t
|
|
64
|
+
.integer()
|
|
65
|
+
.nullable()
|
|
66
|
+
.foreignKey(() => UserModel, 'id'),
|
|
67
|
+
bio: t.text().nullable(),
|
|
68
|
+
...t.timestamps(),
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
relations = {
|
|
72
|
+
user: this.belongsTo(() => UserModel, {
|
|
73
|
+
required: true,
|
|
74
|
+
primaryKey: 'id',
|
|
75
|
+
foreignKey: 'userId',
|
|
76
|
+
}),
|
|
77
|
+
|
|
78
|
+
chats: this.hasMany(() => ChatModel, {
|
|
79
|
+
through: 'user',
|
|
80
|
+
source: 'chats',
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export const ProfileSchema = modelToZod(ProfileModel);
|
|
85
|
+
|
|
86
|
+
export type Chat = ChatModel['columns']['type'];
|
|
87
|
+
export class ChatModel extends Model {
|
|
88
|
+
table = 'chat';
|
|
89
|
+
columns = this.setColumns((t) => ({
|
|
90
|
+
id: t.serial().primaryKey(),
|
|
91
|
+
title: t.text(),
|
|
92
|
+
...t.timestamps(),
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
relations = {
|
|
96
|
+
users: this.hasAndBelongsToMany(() => UserModel, {
|
|
97
|
+
primaryKey: 'id',
|
|
98
|
+
foreignKey: 'chatId',
|
|
99
|
+
associationPrimaryKey: 'id',
|
|
100
|
+
associationForeignKey: 'userId',
|
|
101
|
+
joinTable: 'chatUser',
|
|
102
|
+
}),
|
|
103
|
+
|
|
104
|
+
profiles: this.hasMany(() => ProfileModel, {
|
|
105
|
+
through: 'users',
|
|
106
|
+
source: 'profile',
|
|
107
|
+
}),
|
|
108
|
+
|
|
109
|
+
messages: this.hasMany(() => MessageModel, {
|
|
110
|
+
primaryKey: 'id',
|
|
111
|
+
foreignKey: 'chatId',
|
|
112
|
+
}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export const ChatSchema = modelToZod(ChatModel);
|
|
116
|
+
|
|
117
|
+
export type Message = MessageModel['columns']['type'];
|
|
118
|
+
export class MessageModel extends Model {
|
|
119
|
+
table = 'message';
|
|
120
|
+
columns = this.setColumns((t) => ({
|
|
121
|
+
id: t.serial().primaryKey(),
|
|
122
|
+
chatId: t.integer().foreignKey(() => ChatModel, 'id'),
|
|
123
|
+
authorId: t
|
|
124
|
+
.integer()
|
|
125
|
+
.nullable()
|
|
126
|
+
.foreignKey(() => UserModel, 'id'),
|
|
127
|
+
text: t.text(),
|
|
128
|
+
...t.timestamps(),
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
relations = {
|
|
132
|
+
user: this.belongsTo(() => UserModel, {
|
|
133
|
+
primaryKey: 'id',
|
|
134
|
+
foreignKey: 'authorId',
|
|
135
|
+
}),
|
|
136
|
+
|
|
137
|
+
chat: this.belongsTo(() => ChatModel, {
|
|
138
|
+
primaryKey: 'id',
|
|
139
|
+
foreignKey: 'chatId',
|
|
140
|
+
}),
|
|
141
|
+
|
|
142
|
+
profile: this.hasOne(() => ProfileModel, {
|
|
143
|
+
required: true,
|
|
144
|
+
through: 'user',
|
|
145
|
+
source: 'profile',
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export const MessageSchema = modelToZod(MessageModel);
|
|
150
|
+
|
|
151
|
+
export type Post = PostModel['columns']['type'];
|
|
152
|
+
export class PostModel extends Model {
|
|
153
|
+
table = 'post';
|
|
154
|
+
columns = this.setColumns((t) => ({
|
|
155
|
+
id: t.serial().primaryKey(),
|
|
156
|
+
userId: t.integer().foreignKey(() => UserModel, 'id'),
|
|
157
|
+
title: t.text(),
|
|
158
|
+
...t.timestamps(),
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
relations = {
|
|
162
|
+
postTags: this.hasMany(() => PostTagModel, {
|
|
163
|
+
primaryKey: 'id',
|
|
164
|
+
foreignKey: 'postId',
|
|
165
|
+
}),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
export const PostSchema = modelToZod(PostModel);
|
|
169
|
+
|
|
170
|
+
export type PostTag = PostTagModel['columns']['type'];
|
|
171
|
+
export class PostTagModel extends Model {
|
|
172
|
+
table = 'postTag';
|
|
173
|
+
columns = this.setColumns((t) => ({
|
|
174
|
+
postId: t.integer().foreignKey(() => PostModel, 'id'),
|
|
175
|
+
tag: t.text().foreignKey(() => TagModel, 'tag'),
|
|
176
|
+
...t.primaryKey(['postId', 'tag']),
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
relations = {
|
|
180
|
+
tag: this.belongsTo(() => TagModel, {
|
|
181
|
+
primaryKey: 'tag',
|
|
182
|
+
foreignKey: 'tag',
|
|
183
|
+
}),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export const PostTagSchema = modelToZod(PostTagModel);
|
|
187
|
+
|
|
188
|
+
export type Tag = TagModel['columns']['type'];
|
|
189
|
+
export class TagModel extends Model {
|
|
190
|
+
table = 'tag';
|
|
191
|
+
columns = this.setColumns((t) => ({
|
|
192
|
+
tag: t.text().primaryKey(),
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
patchPgForTransactions,
|
|
3
|
+
rollbackTransaction,
|
|
4
|
+
startTransaction,
|
|
5
|
+
} from 'pg-transactional-tests';
|
|
6
|
+
import { db } from './test-db';
|
|
7
|
+
|
|
8
|
+
type AssertEqual<T, Expected> = [T] extends [Expected]
|
|
9
|
+
? [Expected] extends [T]
|
|
10
|
+
? true
|
|
11
|
+
: false
|
|
12
|
+
: false;
|
|
13
|
+
|
|
14
|
+
export const assertType = <T, Expected>(
|
|
15
|
+
..._: AssertEqual<T, Expected> extends true ? [] : ['invalid type']
|
|
16
|
+
) => {
|
|
17
|
+
// noop
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const line = (s: string) =>
|
|
21
|
+
s.trim().replace(/\s+/g, ' ').replace(/\( /g, '(').replace(/ \)/g, ')');
|
|
22
|
+
|
|
23
|
+
export const expectSql = (
|
|
24
|
+
sql: { text: string; values: unknown[] },
|
|
25
|
+
text: string,
|
|
26
|
+
values: unknown[] = [],
|
|
27
|
+
) => {
|
|
28
|
+
expect(sql.text).toBe(line(text));
|
|
29
|
+
expect(sql.values).toEqual(values);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const toLine = (s: string) => {
|
|
33
|
+
return s.trim().replace(/\n\s*/g, ' ');
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const now = new Date();
|
|
37
|
+
export const userData = {
|
|
38
|
+
name: 'name',
|
|
39
|
+
password: 'password',
|
|
40
|
+
updatedAt: now,
|
|
41
|
+
createdAt: now,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const profileData = {
|
|
45
|
+
bio: 'bio',
|
|
46
|
+
updatedAt: now,
|
|
47
|
+
createdAt: now,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const chatData = {
|
|
51
|
+
title: 'chat',
|
|
52
|
+
updatedAt: now,
|
|
53
|
+
createdAt: now,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const messageData = {
|
|
57
|
+
text: 'text',
|
|
58
|
+
updatedAt: now,
|
|
59
|
+
createdAt: now,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const useTestDatabase = () => {
|
|
63
|
+
beforeAll(patchPgForTransactions);
|
|
64
|
+
beforeEach(startTransaction);
|
|
65
|
+
afterEach(rollbackTransaction);
|
|
66
|
+
afterAll(async () => {
|
|
67
|
+
await db.$close();
|
|
68
|
+
});
|
|
69
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { db } from './test-utils/test-db';
|
|
2
|
+
import { profileData, toLine, userData } from './test-utils/test-utils';
|
|
3
|
+
import { Client } from 'pg';
|
|
4
|
+
import { noop } from 'pqb';
|
|
5
|
+
|
|
6
|
+
describe('transaction', () => {
|
|
7
|
+
it('should have override transaction method which implicitly connects models with a single transaction', async () => {
|
|
8
|
+
const spy = jest.spyOn(Client.prototype, 'query');
|
|
9
|
+
|
|
10
|
+
await db
|
|
11
|
+
.$transaction(async (db) => {
|
|
12
|
+
await db.user.create(userData);
|
|
13
|
+
await db.profile.create(profileData);
|
|
14
|
+
throw new Error('Throw error to rollback');
|
|
15
|
+
})
|
|
16
|
+
.catch(noop);
|
|
17
|
+
|
|
18
|
+
expect(
|
|
19
|
+
spy.mock.calls.map(
|
|
20
|
+
(call) => (call[0] as unknown as { text: string }).text,
|
|
21
|
+
),
|
|
22
|
+
).toEqual([
|
|
23
|
+
'BEGIN',
|
|
24
|
+
toLine(`
|
|
25
|
+
INSERT INTO "user"("name", "password", "updatedAt", "createdAt")
|
|
26
|
+
VALUES ($1, $2, $3, $4)
|
|
27
|
+
RETURNING *
|
|
28
|
+
`),
|
|
29
|
+
toLine(`
|
|
30
|
+
INSERT INTO "profile"("bio", "updatedAt", "createdAt")
|
|
31
|
+
VALUES ($1, $2, $3)
|
|
32
|
+
RETURNING *
|
|
33
|
+
`),
|
|
34
|
+
'ROLLBACK',
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should throw error if argument is forgotten', async () => {
|
|
39
|
+
expect(() =>
|
|
40
|
+
db.$transaction(async () => {
|
|
41
|
+
// noop
|
|
42
|
+
}),
|
|
43
|
+
).toThrow('Argument of $transaction callback should be used');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Db } from 'pqb';
|
|
2
|
+
|
|
3
|
+
export function transaction<T extends { $queryBuilder: Db }, Result>(
|
|
4
|
+
this: T,
|
|
5
|
+
fn: (db: T) => Promise<Result>,
|
|
6
|
+
): Promise<Result> {
|
|
7
|
+
if (fn.length === 0) {
|
|
8
|
+
throw new Error('Argument of $transaction callback should be used');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return this.$queryBuilder.transaction((q) => {
|
|
12
|
+
const orm = {} as T;
|
|
13
|
+
for (const key in this) {
|
|
14
|
+
const value = this[key];
|
|
15
|
+
if (value instanceof Db) {
|
|
16
|
+
const model = value.transacting(q);
|
|
17
|
+
model.__model = model;
|
|
18
|
+
(model as unknown as { db: unknown }).db = orm;
|
|
19
|
+
orm[key] = model;
|
|
20
|
+
} else {
|
|
21
|
+
orm[key] = value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return fn(orm);
|
|
26
|
+
});
|
|
27
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"include": ["./src", "jest-setup.ts"],
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"noEmit": false,
|
|
7
|
+
"baseUrl": ".",
|
|
8
|
+
"paths": {
|
|
9
|
+
"pqb": ["../pqb/src"],
|
|
10
|
+
"orchid-orm-schema-to-zod": ["../schema-to-zod/src"]
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|