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.
@@ -0,0 +1,398 @@
1
+ import { RelationData, RelationThunkBase } from './relations';
2
+ import { Model } from '../model';
3
+ import {
4
+ getQueryAs,
5
+ HasAndBelongsToManyRelation,
6
+ HasManyNestedInsert,
7
+ HasManyNestedUpdate,
8
+ MaybeArray,
9
+ NotFoundError,
10
+ pushQueryValue,
11
+ Query,
12
+ QueryBase,
13
+ toSqlCacheKey,
14
+ WhereArg,
15
+ WhereResult,
16
+ } from 'pqb';
17
+
18
+ export interface HasAndBelongsToMany extends RelationThunkBase {
19
+ type: 'hasAndBelongsToMany';
20
+ returns: 'many';
21
+ options: HasAndBelongsToManyRelation['options'];
22
+ }
23
+
24
+ export type HasAndBelongsToManyInfo<
25
+ T extends Model,
26
+ Relation extends HasAndBelongsToMany,
27
+ > = {
28
+ params: Record<
29
+ Relation['options']['primaryKey'],
30
+ T['columns']['shape'][Relation['options']['primaryKey']]['type']
31
+ >;
32
+ populate: never;
33
+ chainedCreate: true;
34
+ chainedDelete: true;
35
+ };
36
+
37
+ type State = {
38
+ relatedTableQuery: Query;
39
+ joinTableQuery: Query;
40
+ primaryKey: string;
41
+ foreignKey: string;
42
+ associationPrimaryKey: string;
43
+ associationForeignKey: string;
44
+ };
45
+
46
+ export const makeHasAndBelongsToManyMethod = (
47
+ model: Query,
48
+ qb: Query,
49
+ relation: HasAndBelongsToMany,
50
+ query: Query,
51
+ ): RelationData => {
52
+ const {
53
+ primaryKey: pk,
54
+ foreignKey: fk,
55
+ associationPrimaryKey: apk,
56
+ associationForeignKey: afk,
57
+ joinTable,
58
+ } = relation.options;
59
+
60
+ const foreignKeyFull = `${joinTable}.${fk}`;
61
+ const associationForeignKeyFull = `${joinTable}.${afk}`;
62
+ const associationPrimaryKeyFull = `${getQueryAs(query)}.${apk}`;
63
+
64
+ const __model = Object.create(qb.__model);
65
+ __model.__model = __model;
66
+ __model.table = joinTable;
67
+ __model.shape = {
68
+ [fk]: model.shape[pk],
69
+ [afk]: query.shape[apk],
70
+ };
71
+ const subQuery = Object.create(__model);
72
+ subQuery.query = { ...subQuery.query };
73
+
74
+ const state: State = {
75
+ relatedTableQuery: query,
76
+ joinTableQuery: subQuery,
77
+ primaryKey: pk,
78
+ foreignKey: fk,
79
+ associationPrimaryKey: apk,
80
+ associationForeignKey: afk,
81
+ };
82
+
83
+ return {
84
+ returns: 'many',
85
+ method(params: Record<string, unknown>) {
86
+ return query.whereExists(subQuery, (q) =>
87
+ q.on(associationForeignKeyFull, associationPrimaryKeyFull).where({
88
+ [foreignKeyFull]: params[pk],
89
+ }),
90
+ );
91
+ },
92
+ nestedInsert: (async (q, data) => {
93
+ const connect = data.filter(
94
+ (
95
+ item,
96
+ ): item is [
97
+ selfData: Record<string, unknown>,
98
+ relationData: {
99
+ connect: WhereArg<QueryBase>[];
100
+ },
101
+ ] => Boolean(item[1].connect),
102
+ );
103
+
104
+ const t = query.transacting(q);
105
+
106
+ let connected: Record<string, unknown>[];
107
+ if (connect.length) {
108
+ connected = (await Promise.all(
109
+ connect.flatMap(([, { connect }]) =>
110
+ connect.map((item) => t.select(apk)._findBy(item)._take()),
111
+ ),
112
+ )) as Record<string, unknown[]>[];
113
+ } else {
114
+ connected = [];
115
+ }
116
+
117
+ const connectOrCreate = data.filter(
118
+ (
119
+ item,
120
+ ): item is [
121
+ Record<string, unknown>,
122
+ {
123
+ connectOrCreate: {
124
+ where: WhereArg<QueryBase>;
125
+ create: Record<string, unknown>;
126
+ }[];
127
+ },
128
+ ] => Boolean(item[1].connectOrCreate),
129
+ );
130
+
131
+ let connectOrCreated: (Record<string, unknown> | undefined)[];
132
+ if (connectOrCreate.length) {
133
+ connectOrCreated = await Promise.all(
134
+ connectOrCreate.flatMap(([, { connectOrCreate }]) =>
135
+ connectOrCreate.map((item) =>
136
+ t.select(apk)._findBy(item.where)._takeOptional(),
137
+ ),
138
+ ),
139
+ );
140
+ } else {
141
+ connectOrCreated = [];
142
+ }
143
+
144
+ let connectOrCreateI = 0;
145
+ const create = data.filter(
146
+ (
147
+ item,
148
+ ): item is [
149
+ Record<string, unknown>,
150
+ {
151
+ create?: Record<string, unknown>[];
152
+ connectOrCreate?: {
153
+ where: WhereArg<QueryBase>;
154
+ create: Record<string, unknown>;
155
+ }[];
156
+ },
157
+ ] => {
158
+ if (item[1].connectOrCreate) {
159
+ const length = item[1].connectOrCreate.length;
160
+ connectOrCreateI += length;
161
+ for (let i = length; i > 0; i--) {
162
+ if (!connectOrCreated[connectOrCreateI - i]) return true;
163
+ }
164
+ }
165
+ return Boolean(item[1].create);
166
+ },
167
+ );
168
+
169
+ connectOrCreateI = 0;
170
+ let created: Record<string, unknown>[];
171
+ if (create.length) {
172
+ created = (await t
173
+ .select(apk)
174
+ ._createMany(
175
+ create.flatMap(([, { create = [], connectOrCreate = [] }]) => [
176
+ ...create,
177
+ ...connectOrCreate
178
+ .filter(() => !connectOrCreated[connectOrCreateI++])
179
+ .map((item) => item.create),
180
+ ]),
181
+ )) as Record<string, unknown>[];
182
+ } else {
183
+ created = [];
184
+ }
185
+
186
+ const allKeys = data as unknown as [
187
+ selfData: Record<string, unknown>,
188
+ relationKeys: Record<string, unknown>[],
189
+ ][];
190
+
191
+ let createI = 0;
192
+ let connectI = 0;
193
+ connectOrCreateI = 0;
194
+ data.forEach(([, data], index) => {
195
+ if (data.create || data.connectOrCreate) {
196
+ if (data.create) {
197
+ const len = data.create.length;
198
+ allKeys[index][1] = created.slice(createI, createI + len);
199
+ createI += len;
200
+ }
201
+ if (data.connectOrCreate) {
202
+ const arr: Record<string, unknown>[] = [];
203
+ allKeys[index][1] = arr;
204
+
205
+ const len = data.connectOrCreate.length;
206
+ for (let i = 0; i < len; i++) {
207
+ const item = connectOrCreated[connectOrCreateI++];
208
+ if (item) {
209
+ arr.push(item);
210
+ } else {
211
+ arr.push(created[createI++]);
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ if (data.connect) {
218
+ const len = data.connect.length;
219
+ allKeys[index][1] = connected.slice(connectI, connectI + len);
220
+ connectI += len;
221
+ }
222
+ });
223
+
224
+ await subQuery
225
+ .transacting(q)
226
+ ._count()
227
+ ._createMany(
228
+ allKeys.flatMap(([selfData, relationKeys]) => {
229
+ const selfKey = selfData[pk];
230
+ return relationKeys.map((relationData) => ({
231
+ [fk]: selfKey,
232
+ [afk]: relationData[apk],
233
+ }));
234
+ }),
235
+ );
236
+ }) as HasManyNestedInsert,
237
+ nestedUpdate: (async (q, data, params) => {
238
+ if (params.create) {
239
+ const ids = await query
240
+ .transacting(q)
241
+ ._pluck(apk)
242
+ ._createMany(params.create);
243
+
244
+ await subQuery.transacting(q)._createMany(
245
+ data.flatMap((item) =>
246
+ ids.map((id) => ({
247
+ [fk]: item[pk],
248
+ [afk]: id,
249
+ })),
250
+ ),
251
+ );
252
+ }
253
+
254
+ if (params.update) {
255
+ await (
256
+ query
257
+ .transacting(q)
258
+ ._whereExists(subQuery, (q) =>
259
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
260
+ (q as any)
261
+ ._on(associationForeignKeyFull, associationPrimaryKeyFull)
262
+ ._where({
263
+ IN: {
264
+ columns: [foreignKeyFull],
265
+ values: [data.map((item) => item[pk])],
266
+ },
267
+ }),
268
+ )
269
+ ._where(
270
+ Array.isArray(params.update.where)
271
+ ? { OR: params.update.where }
272
+ : params.update.where,
273
+ ) as WhereResult<Query>
274
+ )._update<WhereResult<Query>>(params.update.data);
275
+ }
276
+
277
+ if (params.disconnect) {
278
+ await queryJoinTable(state, q, data, params.disconnect)._delete();
279
+ }
280
+
281
+ if (params.delete) {
282
+ const j = queryJoinTable(state, q, data, params.delete);
283
+
284
+ const ids = await j._pluck(afk)._delete();
285
+
286
+ await queryRelatedTable(query, q, { [apk]: { in: ids } })._delete();
287
+ }
288
+
289
+ if (params.set) {
290
+ const j = queryJoinTable(state, q, data);
291
+ await j._delete();
292
+ delete j.query[toSqlCacheKey];
293
+
294
+ const ids = await queryRelatedTable(query, q, params.set)._pluck(apk);
295
+
296
+ await insertToJoinTable(state, j, data, ids);
297
+ }
298
+ }) as HasManyNestedUpdate,
299
+ // joinQuery can be a property of RelationQuery and be used by whereExists and other stuff which needs it
300
+ // and the chained query itself may be a query around this joinQuery
301
+ joinQuery(fromQuery, toQuery) {
302
+ return toQuery.whereExists(subQuery, (q) =>
303
+ q
304
+ ._on(associationForeignKeyFull, `${getQueryAs(toQuery)}.${pk}`)
305
+ ._on(foreignKeyFull, `${getQueryAs(fromQuery)}.${pk}`),
306
+ );
307
+ },
308
+ reverseJoin(fromQuery, toQuery) {
309
+ return fromQuery.whereExists(subQuery, (q) =>
310
+ q
311
+ ._on(associationForeignKeyFull, `${getQueryAs(toQuery)}.${pk}`)
312
+ ._on(foreignKeyFull, `${getQueryAs(fromQuery)}.${pk}`),
313
+ );
314
+ },
315
+ primaryKey: pk,
316
+ modifyRelatedQuery(relationQuery) {
317
+ const ref = {} as { query: Query };
318
+
319
+ pushQueryValue(
320
+ relationQuery,
321
+ 'afterCreate',
322
+ async (q: Query, result: Record<string, unknown>) => {
323
+ const fromQuery = ref.query.clone();
324
+ fromQuery.query.select = [{ selectAs: { [fk]: pk } }];
325
+
326
+ const createdCount = await subQuery
327
+ .transacting(q)
328
+ .count()
329
+ ._createFrom(
330
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
331
+ fromQuery as any,
332
+ {
333
+ [afk]: result[apk],
334
+ } as never,
335
+ );
336
+
337
+ if (createdCount === 0) {
338
+ throw new NotFoundError();
339
+ }
340
+ },
341
+ );
342
+
343
+ return (q) => {
344
+ ref.query = q;
345
+ };
346
+ },
347
+ };
348
+ };
349
+
350
+ const queryJoinTable = (
351
+ state: State,
352
+ q: Query,
353
+ data: Record<string, unknown>[],
354
+ conditions?: MaybeArray<WhereArg<Query>>,
355
+ ) => {
356
+ const t = state.joinTableQuery.transacting(q);
357
+ const where: WhereArg<Query> = {
358
+ [state.foreignKey]: { in: data.map((item) => item[state.primaryKey]) },
359
+ };
360
+
361
+ if (conditions) {
362
+ where[state.associationForeignKey] = {
363
+ in: state.relatedTableQuery
364
+ .where<Query>(
365
+ Array.isArray(conditions) ? { OR: conditions } : conditions,
366
+ )
367
+ ._select(state.associationPrimaryKey),
368
+ };
369
+ }
370
+
371
+ return t._where(where);
372
+ };
373
+
374
+ const queryRelatedTable = (
375
+ query: Query,
376
+ q: Query,
377
+ conditions: MaybeArray<WhereArg<Query>>,
378
+ ) => {
379
+ return query
380
+ .transacting(q)
381
+ ._where<Query>(Array.isArray(conditions) ? { OR: conditions } : conditions);
382
+ };
383
+
384
+ const insertToJoinTable = (
385
+ state: State,
386
+ joinTableTransaction: Query,
387
+ data: Record<string, unknown>[],
388
+ ids: unknown[],
389
+ ) => {
390
+ return joinTableTransaction._count()._createMany(
391
+ data.flatMap((item) =>
392
+ ids.map((id) => ({
393
+ [state.foreignKey]: item[state.primaryKey],
394
+ [state.associationForeignKey]: id,
395
+ })),
396
+ ),
397
+ );
398
+ };