orchid-orm 1.3.15 → 1.3.16

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.
@@ -1,14 +1,15 @@
1
1
  import {
2
2
  addQueryOn,
3
+ CreateCtx,
3
4
  getQueryAs,
4
- HasOneNestedInsert,
5
- HasOneNestedUpdate,
6
5
  HasOneRelation,
7
6
  InsertQueryData,
8
7
  isQueryReturnsAll,
9
8
  JoinCallback,
10
9
  Query,
11
10
  QueryBase,
11
+ UpdateCtx,
12
+ VirtualColumn,
12
13
  WhereArg,
13
14
  WhereResult,
14
15
  } from 'pqb';
@@ -19,7 +20,14 @@ import {
19
20
  RelationThunkBase,
20
21
  RelationThunks,
21
22
  } from './relations';
22
- import { getSourceRelation, getThroughRelation } from './utils';
23
+ import {
24
+ getSourceRelation,
25
+ getThroughRelation,
26
+ hasRelationHandleCreate,
27
+ hasRelationHandleUpdate,
28
+ NestedInsertOneItem,
29
+ NestedUpdateOneItem,
30
+ } from './utils';
23
31
 
24
32
  export interface HasOne extends RelationThunkBase {
25
33
  type: 'hasOne';
@@ -53,6 +61,65 @@ export type HasOneInfo<
53
61
  chainedDelete: true;
54
62
  };
55
63
 
64
+ type State = {
65
+ query: Query;
66
+ primaryKey: string;
67
+ foreignKey: string;
68
+ };
69
+
70
+ export type HasOneNestedInsert = (
71
+ query: Query,
72
+ data: [
73
+ selfData: Record<string, unknown>,
74
+ relationData: NestedInsertOneItem,
75
+ ][],
76
+ ) => Promise<void>;
77
+
78
+ export type HasOneNestedUpdate = (
79
+ query: Query,
80
+ data: Record<string, unknown>[],
81
+ relationData: NestedUpdateOneItem,
82
+ ) => Promise<void>;
83
+
84
+ class HasOneVirtualColumn extends VirtualColumn {
85
+ private readonly nestedInsert: HasOneNestedInsert;
86
+ private readonly nestedUpdate: HasOneNestedUpdate;
87
+
88
+ constructor(private key: string, private state: State) {
89
+ super();
90
+ this.nestedInsert = nestedInsert(state);
91
+ this.nestedUpdate = nestedUpdate(state);
92
+ }
93
+
94
+ create(
95
+ q: Query,
96
+ ctx: CreateCtx,
97
+ item: Record<string, unknown>,
98
+ rowIndex: number,
99
+ ) {
100
+ hasRelationHandleCreate(
101
+ q,
102
+ ctx,
103
+ item,
104
+ rowIndex,
105
+ this.key,
106
+ this.state.primaryKey,
107
+ this.nestedInsert,
108
+ );
109
+ }
110
+
111
+ update(q: Query, ctx: UpdateCtx, set: Record<string, unknown>) {
112
+ hasRelationHandleUpdate(
113
+ q,
114
+ ctx,
115
+ set,
116
+ this.key,
117
+ this.state.primaryKey,
118
+ this.nestedUpdate,
119
+ );
120
+ }
121
+ }
122
+
56
123
  export const makeHasOneMethod = (
57
124
  model: Query,
58
125
  relation: HasOne,
@@ -93,8 +160,6 @@ export const makeHasOneMethod = (
93
160
  whereExistsCallback as unknown as JoinCallback<Query, Query>,
94
161
  );
95
162
  },
96
- nestedInsert: undefined,
97
- nestedUpdate: undefined,
98
163
  joinQuery(fromQuery, toQuery) {
99
164
  return toQuery.whereExists<Query, Query>(
100
165
  throughRelation.joinQuery(fromQuery, throughRelation.query),
@@ -124,6 +189,7 @@ export const makeHasOneMethod = (
124
189
  }
125
190
 
126
191
  const { primaryKey, foreignKey } = relation.options;
192
+ const state: State = { query, primaryKey, foreignKey };
127
193
 
128
194
  const fromQuerySelect = [{ selectAs: { [foreignKey]: primaryKey } }];
129
195
 
@@ -133,133 +199,7 @@ export const makeHasOneMethod = (
133
199
  const values = { [foreignKey]: params[primaryKey] };
134
200
  return query.where(values)._defaults(values);
135
201
  },
136
- nestedInsert: (async (q, data) => {
137
- const connect = data.filter(
138
- (
139
- item,
140
- ): item is [
141
- Record<string, unknown>,
142
- (
143
- | {
144
- connect: WhereArg<QueryBase>;
145
- }
146
- | {
147
- connectOrCreate: {
148
- where: WhereArg<QueryBase>;
149
- create: Record<string, unknown>;
150
- };
151
- }
152
- ),
153
- ] => Boolean(item[1].connect || item[1].connectOrCreate),
154
- );
155
-
156
- const t = query.transacting(q);
157
-
158
- let connected: number[];
159
- if (connect.length) {
160
- connected = await Promise.all(
161
- connect.map(([selfData, item]) => {
162
- const data = { [foreignKey]: selfData[primaryKey] };
163
- return 'connect' in item
164
- ? (
165
- t.where(item.connect) as WhereResult<Query> & {
166
- hasSelect: false;
167
- }
168
- )._updateOrThrow(data)
169
- : (
170
- t.where(item.connectOrCreate.where) as WhereResult<Query> & {
171
- hasSelect: false;
172
- }
173
- )._update(data);
174
- }),
175
- );
176
- } else {
177
- connected = [];
178
- }
179
-
180
- let connectedI = 0;
181
- const create = data.filter(
182
- (
183
- item,
184
- ): item is [
185
- Record<string, unknown>,
186
- (
187
- | { create: Record<string, unknown> }
188
- | {
189
- connectOrCreate: {
190
- where: WhereArg<QueryBase>;
191
- create: Record<string, unknown>;
192
- };
193
- }
194
- ),
195
- ] => {
196
- if (item[1].connectOrCreate) {
197
- return !connected[connectedI++];
198
- }
199
- return Boolean(item[1].create);
200
- },
201
- );
202
-
203
- if (create.length) {
204
- await t._count()._createMany(
205
- create.map(([selfData, item]) => ({
206
- [foreignKey]: selfData[primaryKey],
207
- ...('create' in item ? item.create : item.connectOrCreate.create),
208
- })),
209
- );
210
- }
211
- }) as HasOneNestedInsert,
212
- nestedUpdate: (async (q, data, params) => {
213
- if (
214
- (params.set || params.create || params.upsert) &&
215
- isQueryReturnsAll(q)
216
- ) {
217
- const key = params.set ? 'set' : params.create ? 'create' : 'upsert';
218
- throw new Error(`\`${key}\` option is not allowed in a batch update`);
219
- }
220
-
221
- const t = query.transacting(q);
222
- const ids = data.map((item) => item[primaryKey]);
223
- const currentRelationsQuery = t.where({
224
- [foreignKey]: { in: ids },
225
- });
226
-
227
- if (params.create || params.disconnect || params.set) {
228
- await currentRelationsQuery._update({ [foreignKey]: null });
229
-
230
- if (params.create) {
231
- await t._count()._create({
232
- ...params.create,
233
- [foreignKey]: data[0][primaryKey],
234
- });
235
- }
236
- if (params.set) {
237
- await t
238
- ._where<Query>(params.set)
239
- ._update({ [foreignKey]: data[0][primaryKey] });
240
- }
241
- } else if (params.update) {
242
- await currentRelationsQuery._update<WhereResult<Query>>(params.update);
243
- } else if (params.delete) {
244
- await currentRelationsQuery._delete();
245
- } else if (params.upsert) {
246
- const { update, create } = params.upsert;
247
- const updatedIds: unknown[] = await currentRelationsQuery
248
- ._pluck(foreignKey)
249
- ._update<WhereResult<Query & { hasSelect: true }>>(update);
250
-
251
- if (updatedIds.length < ids.length) {
252
- await t.createMany(
253
- ids
254
- .filter((id) => !updatedIds.includes(id))
255
- .map((id) => ({
256
- ...create,
257
- [foreignKey]: id,
258
- })),
259
- );
260
- }
261
- }
262
- }) as HasOneNestedUpdate,
202
+ virtualColumn: new HasOneVirtualColumn(relationName, state),
263
203
  joinQuery(fromQuery, toQuery) {
264
204
  return addQueryOn(toQuery, fromQuery, toQuery, foreignKey, primaryKey);
265
205
  },
@@ -276,3 +216,136 @@ export const makeHasOneMethod = (
276
216
  },
277
217
  };
278
218
  };
219
+
220
+ const nestedInsert = ({ query, primaryKey, foreignKey }: State) => {
221
+ return (async (q, data) => {
222
+ const connect = data.filter(
223
+ (
224
+ item,
225
+ ): item is [
226
+ Record<string, unknown>,
227
+ (
228
+ | {
229
+ connect: WhereArg<QueryBase>;
230
+ }
231
+ | {
232
+ connectOrCreate: {
233
+ where: WhereArg<QueryBase>;
234
+ create: Record<string, unknown>;
235
+ };
236
+ }
237
+ ),
238
+ ] => Boolean(item[1].connect || item[1].connectOrCreate),
239
+ );
240
+
241
+ const t = query.transacting(q);
242
+
243
+ let connected: number[];
244
+ if (connect.length) {
245
+ connected = await Promise.all(
246
+ connect.map(([selfData, item]) => {
247
+ const data = { [foreignKey]: selfData[primaryKey] };
248
+ return 'connect' in item
249
+ ? (
250
+ t.where(item.connect) as WhereResult<Query> & {
251
+ hasSelect: false;
252
+ }
253
+ )._updateOrThrow(data)
254
+ : (
255
+ t.where(item.connectOrCreate.where) as WhereResult<Query> & {
256
+ hasSelect: false;
257
+ }
258
+ )._update(data);
259
+ }),
260
+ );
261
+ } else {
262
+ connected = [];
263
+ }
264
+
265
+ let connectedI = 0;
266
+ const create = data.filter(
267
+ (
268
+ item,
269
+ ): item is [
270
+ Record<string, unknown>,
271
+ (
272
+ | { create: Record<string, unknown> }
273
+ | {
274
+ connectOrCreate: {
275
+ where: WhereArg<QueryBase>;
276
+ create: Record<string, unknown>;
277
+ };
278
+ }
279
+ ),
280
+ ] => {
281
+ if (item[1].connectOrCreate) {
282
+ return !connected[connectedI++];
283
+ }
284
+ return Boolean(item[1].create);
285
+ },
286
+ );
287
+
288
+ if (create.length) {
289
+ await t._count()._createMany(
290
+ create.map(([selfData, item]) => ({
291
+ [foreignKey]: selfData[primaryKey],
292
+ ...('create' in item ? item.create : item.connectOrCreate.create),
293
+ })),
294
+ );
295
+ }
296
+ }) as HasOneNestedInsert;
297
+ };
298
+
299
+ const nestedUpdate = ({ query, primaryKey, foreignKey }: State) => {
300
+ return (async (q, data, params) => {
301
+ if (
302
+ (params.set || params.create || params.upsert) &&
303
+ isQueryReturnsAll(q)
304
+ ) {
305
+ const key = params.set ? 'set' : params.create ? 'create' : 'upsert';
306
+ throw new Error(`\`${key}\` option is not allowed in a batch update`);
307
+ }
308
+
309
+ const t = query.transacting(q);
310
+ const ids = data.map((item) => item[primaryKey]);
311
+ const currentRelationsQuery = t.where({
312
+ [foreignKey]: { in: ids },
313
+ });
314
+
315
+ if (params.create || params.disconnect || params.set) {
316
+ await currentRelationsQuery._update({ [foreignKey]: null });
317
+
318
+ if (params.create) {
319
+ await t._count()._create({
320
+ ...params.create,
321
+ [foreignKey]: data[0][primaryKey],
322
+ });
323
+ }
324
+ if (params.set) {
325
+ await t
326
+ ._where<Query>(params.set)
327
+ ._update({ [foreignKey]: data[0][primaryKey] });
328
+ }
329
+ } else if (params.update) {
330
+ await currentRelationsQuery._update<WhereResult<Query>>(params.update);
331
+ } else if (params.delete) {
332
+ await currentRelationsQuery._delete();
333
+ } else if (params.upsert) {
334
+ const { update, create } = params.upsert;
335
+ const updatedIds: unknown[] = await currentRelationsQuery
336
+ ._pluck(foreignKey)
337
+ ._update<WhereResult<Query & { hasSelect: true }>>(update);
338
+
339
+ if (updatedIds.length < ids.length) {
340
+ await t.createMany(
341
+ ids
342
+ .filter((id) => !updatedIds.includes(id))
343
+ .map((id) => ({
344
+ ...create,
345
+ [foreignKey]: id,
346
+ })),
347
+ );
348
+ }
349
+ }
350
+ }) as HasOneNestedUpdate;
351
+ };
@@ -13,6 +13,7 @@ import {
13
13
  defaultsKey,
14
14
  relationQueryKey,
15
15
  EmptyObject,
16
+ VirtualColumn,
16
17
  } from 'pqb';
17
18
  import { HasMany, HasManyInfo, makeHasManyMethod } from './hasMany';
18
19
  import {
@@ -36,8 +37,7 @@ export type RelationThunks = Record<string, RelationThunk>;
36
37
  export type RelationData = {
37
38
  returns: 'one' | 'many';
38
39
  method(params: Record<string, unknown>): Query;
39
- nestedInsert: BaseRelation['nestedInsert'];
40
- nestedUpdate: BaseRelation['nestedUpdate'];
40
+ virtualColumn?: VirtualColumn;
41
41
  joinQuery(fromQuery: Query, toQuery: Query): Query;
42
42
  reverseJoin(fromQuery: Query, toQuery: Query): Query;
43
43
  primaryKey: string;
@@ -63,8 +63,6 @@ export type Relation<
63
63
  : M & {
64
64
  [defaultsKey]: Record<Info['populate'], true>;
65
65
  };
66
- nestedInsert: BaseRelation['nestedInsert'];
67
- nestedUpdate: BaseRelation['nestedUpdate'];
68
66
  primaryKey: string;
69
67
  options: Relations[K]['options'];
70
68
  };
@@ -280,13 +278,19 @@ const applyRelation = (
280
278
  const { type } = relation;
281
279
  let data;
282
280
  if (type === 'belongsTo') {
283
- data = makeBelongsToMethod(relation, query);
281
+ data = makeBelongsToMethod(relation, relationName, query);
284
282
  } else if (type === 'hasOne') {
285
283
  data = makeHasOneMethod(dbModel, relation, relationName, query);
286
284
  } else if (type === 'hasMany') {
287
285
  data = makeHasManyMethod(dbModel, relation, relationName, query);
288
286
  } else if (type === 'hasAndBelongsToMany') {
289
- data = makeHasAndBelongsToManyMethod(dbModel, qb, relation, query);
287
+ data = makeHasAndBelongsToManyMethod(
288
+ dbModel,
289
+ qb,
290
+ relation,
291
+ relationName,
292
+ query,
293
+ );
290
294
  } else {
291
295
  throw new Error(`Unknown relation type ${type}`);
292
296
  }
@@ -295,6 +299,10 @@ const applyRelation = (
295
299
  query._take();
296
300
  }
297
301
 
302
+ if (data.virtualColumn) {
303
+ dbModel.shape[relationName] = data.virtualColumn;
304
+ }
305
+
298
306
  makeRelationQuery(dbModel, definedAs, relationName, data);
299
307
 
300
308
  (dbModel.relations as Record<string, unknown>)[relationName] = {
@@ -302,8 +310,6 @@ const applyRelation = (
302
310
  key: relationName,
303
311
  model: otherDbModel,
304
312
  query,
305
- nestedInsert: data.nestedInsert,
306
- nestedUpdate: data.nestedUpdate,
307
313
  joinQuery: data.joinQuery,
308
314
  primaryKey: data.primaryKey,
309
315
  options: relation.options,
@@ -1,4 +1,61 @@
1
- import { Query, Relation } from 'pqb';
1
+ import {
2
+ CreateCtx,
3
+ MaybeArray,
4
+ pushQueryValue,
5
+ Query,
6
+ QueryBase,
7
+ Relation,
8
+ UpdateCtx,
9
+ UpdateData,
10
+ WhereArg,
11
+ } from 'pqb';
12
+ import { HasOneNestedInsert, HasOneNestedUpdate } from './hasOne';
13
+ import { HasManyNestedInsert, HasManyNestedUpdate } from './hasMany';
14
+
15
+ export type NestedInsertOneItem = {
16
+ create?: Record<string, unknown>;
17
+ connect?: WhereArg<QueryBase>;
18
+ connectOrCreate?: {
19
+ where: WhereArg<QueryBase>;
20
+ create: Record<string, unknown>;
21
+ };
22
+ };
23
+
24
+ export type NestedInsertManyItems = {
25
+ create?: Record<string, unknown>[];
26
+ connect?: WhereArg<QueryBase>[];
27
+ connectOrCreate?: {
28
+ where: WhereArg<QueryBase>;
29
+ create: Record<string, unknown>;
30
+ }[];
31
+ };
32
+
33
+ export type NestedInsertItem = NestedInsertOneItem | NestedInsertManyItems;
34
+
35
+ export type NestedUpdateOneItem = {
36
+ disconnect?: boolean;
37
+ set?: WhereArg<QueryBase>;
38
+ delete?: boolean;
39
+ update?: UpdateData<Query>;
40
+ upsert?: {
41
+ update: UpdateData<Query>;
42
+ create: Record<string, unknown>;
43
+ };
44
+ create: Record<string, unknown>;
45
+ };
46
+
47
+ export type NestedUpdateManyItems = {
48
+ disconnect?: MaybeArray<WhereArg<QueryBase>>;
49
+ set?: MaybeArray<WhereArg<QueryBase>>;
50
+ delete?: MaybeArray<WhereArg<QueryBase>>;
51
+ update?: {
52
+ where: MaybeArray<WhereArg<QueryBase>>;
53
+ data: UpdateData<Query>;
54
+ };
55
+ create: Record<string, unknown>[];
56
+ };
57
+
58
+ export type NestedUpdateItem = NestedUpdateOneItem | NestedUpdateManyItems;
2
59
 
3
60
  export const getThroughRelation = (model: Query, through: string) => {
4
61
  return (model.relations as Record<string, Relation>)[through];
@@ -10,3 +67,96 @@ export const getSourceRelation = (
10
67
  ) => {
11
68
  return (throughRelation.model.relations as Record<string, Relation>)[source];
12
69
  };
70
+
71
+ export const hasRelationHandleCreate = (
72
+ q: Query,
73
+ ctx: CreateCtx,
74
+ item: Record<string, unknown>,
75
+ rowIndex: number,
76
+ key: string,
77
+ primaryKey: string,
78
+ nestedInsert: HasOneNestedInsert | HasManyNestedInsert,
79
+ ) => {
80
+ const value = item[key] as NestedInsertItem;
81
+ if (
82
+ (!value.create ||
83
+ (Array.isArray(value.create) && value.create.length === 0)) &&
84
+ (!value.connect ||
85
+ (Array.isArray(value.connect) && value.connect.length === 0)) &&
86
+ (!value.connectOrCreate ||
87
+ (Array.isArray(value.connectOrCreate) &&
88
+ value.connectOrCreate.length === 0))
89
+ )
90
+ return;
91
+
92
+ const store = ctx as unknown as {
93
+ hasRelation?: Record<string, [number, NestedInsertItem][]>;
94
+ };
95
+
96
+ if (!store.hasRelation) store.hasRelation = {};
97
+
98
+ const values = [rowIndex, value] as [number, NestedInsertItem];
99
+
100
+ if (store.hasRelation[key]) {
101
+ store.hasRelation[key].push(values);
102
+ return;
103
+ }
104
+
105
+ q.query.wrapInTransaction = true;
106
+ ctx.returnTypeAll = true;
107
+ ctx.requiredReturning[primaryKey] = true;
108
+
109
+ const relationData = [values];
110
+ store.hasRelation[key] = relationData;
111
+
112
+ pushQueryValue(q, 'afterCreate', async (q: Query) => {
113
+ const { resultAll } = ctx;
114
+ return (nestedInsert as HasOneNestedInsert)(
115
+ q,
116
+ relationData.map(([rowIndex, data]) => [
117
+ resultAll[rowIndex],
118
+ data as NestedInsertOneItem,
119
+ ]),
120
+ );
121
+ });
122
+ };
123
+
124
+ export const hasRelationHandleUpdate = (
125
+ q: Query,
126
+ ctx: UpdateCtx,
127
+ set: Record<string, unknown>,
128
+ key: string,
129
+ primaryKey: string,
130
+ nestedUpdate: HasOneNestedUpdate | HasManyNestedUpdate,
131
+ ) => {
132
+ const value = set[key] as NestedUpdateItem;
133
+
134
+ if (
135
+ !value.set &&
136
+ !('upsert' in value) &&
137
+ (!value.disconnect ||
138
+ (Array.isArray(value.disconnect) && value.disconnect.length === 0)) &&
139
+ (!value.delete ||
140
+ (Array.isArray(value.delete) && value.delete.length === 0)) &&
141
+ (!value.update ||
142
+ (Array.isArray(value.update.where) && value.update.where.length === 0)) &&
143
+ (!value.create ||
144
+ (Array.isArray(value.create) && value.create.length === 0))
145
+ )
146
+ return;
147
+
148
+ if (!q.query.select?.includes('*') && !q.query.select?.includes(primaryKey)) {
149
+ q._select(primaryKey);
150
+ }
151
+
152
+ q.query.wrapInTransaction = true;
153
+ ctx.returnTypeAll = true;
154
+
155
+ pushQueryValue(q, 'afterUpdate', (q: Query) => {
156
+ return (nestedUpdate as HasOneNestedUpdate)(
157
+ q,
158
+ ctx.resultAll,
159
+ value as NestedUpdateOneItem,
160
+ );
161
+ });
162
+ };