@tanstack/db 0.0.4 → 0.0.6

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 (102) hide show
  1. package/dist/cjs/collection.cjs +182 -113
  2. package/dist/cjs/collection.cjs.map +1 -1
  3. package/dist/cjs/collection.d.cts +43 -15
  4. package/dist/cjs/index.cjs +1 -0
  5. package/dist/cjs/index.cjs.map +1 -1
  6. package/dist/cjs/proxy.cjs +87 -248
  7. package/dist/cjs/proxy.cjs.map +1 -1
  8. package/dist/cjs/proxy.d.cts +5 -5
  9. package/dist/cjs/query/compiled-query.cjs +23 -14
  10. package/dist/cjs/query/compiled-query.cjs.map +1 -1
  11. package/dist/cjs/query/compiled-query.d.cts +3 -1
  12. package/dist/cjs/query/evaluators.cjs +35 -20
  13. package/dist/cjs/query/evaluators.cjs.map +1 -1
  14. package/dist/cjs/query/evaluators.d.cts +8 -3
  15. package/dist/cjs/query/extractors.cjs +20 -20
  16. package/dist/cjs/query/extractors.cjs.map +1 -1
  17. package/dist/cjs/query/extractors.d.cts +3 -3
  18. package/dist/cjs/query/group-by.cjs +12 -15
  19. package/dist/cjs/query/group-by.cjs.map +1 -1
  20. package/dist/cjs/query/group-by.d.cts +7 -7
  21. package/dist/cjs/query/joins.cjs +41 -55
  22. package/dist/cjs/query/joins.cjs.map +1 -1
  23. package/dist/cjs/query/joins.d.cts +3 -3
  24. package/dist/cjs/query/order-by.cjs +37 -84
  25. package/dist/cjs/query/order-by.cjs.map +1 -1
  26. package/dist/cjs/query/order-by.d.cts +2 -2
  27. package/dist/cjs/query/pipeline-compiler.cjs +13 -18
  28. package/dist/cjs/query/pipeline-compiler.cjs.map +1 -1
  29. package/dist/cjs/query/pipeline-compiler.d.cts +2 -1
  30. package/dist/cjs/query/query-builder.cjs +22 -29
  31. package/dist/cjs/query/query-builder.cjs.map +1 -1
  32. package/dist/cjs/query/query-builder.d.cts +16 -10
  33. package/dist/cjs/query/schema.d.cts +12 -11
  34. package/dist/cjs/query/select.cjs +47 -24
  35. package/dist/cjs/query/select.cjs.map +1 -1
  36. package/dist/cjs/query/select.d.cts +2 -2
  37. package/dist/cjs/query/types.d.cts +1 -0
  38. package/dist/cjs/transactions.cjs +20 -9
  39. package/dist/cjs/transactions.cjs.map +1 -1
  40. package/dist/cjs/types.d.cts +66 -7
  41. package/dist/esm/collection.d.ts +43 -15
  42. package/dist/esm/collection.js +183 -114
  43. package/dist/esm/collection.js.map +1 -1
  44. package/dist/esm/index.js +2 -1
  45. package/dist/esm/proxy.d.ts +5 -5
  46. package/dist/esm/proxy.js +87 -248
  47. package/dist/esm/proxy.js.map +1 -1
  48. package/dist/esm/query/compiled-query.d.ts +3 -1
  49. package/dist/esm/query/compiled-query.js +23 -14
  50. package/dist/esm/query/compiled-query.js.map +1 -1
  51. package/dist/esm/query/evaluators.d.ts +8 -3
  52. package/dist/esm/query/evaluators.js +36 -21
  53. package/dist/esm/query/evaluators.js.map +1 -1
  54. package/dist/esm/query/extractors.d.ts +3 -3
  55. package/dist/esm/query/extractors.js +20 -20
  56. package/dist/esm/query/extractors.js.map +1 -1
  57. package/dist/esm/query/group-by.d.ts +7 -7
  58. package/dist/esm/query/group-by.js +14 -17
  59. package/dist/esm/query/group-by.js.map +1 -1
  60. package/dist/esm/query/joins.d.ts +3 -3
  61. package/dist/esm/query/joins.js +42 -56
  62. package/dist/esm/query/joins.js.map +1 -1
  63. package/dist/esm/query/order-by.d.ts +2 -2
  64. package/dist/esm/query/order-by.js +39 -86
  65. package/dist/esm/query/order-by.js.map +1 -1
  66. package/dist/esm/query/pipeline-compiler.d.ts +2 -1
  67. package/dist/esm/query/pipeline-compiler.js +14 -19
  68. package/dist/esm/query/pipeline-compiler.js.map +1 -1
  69. package/dist/esm/query/query-builder.d.ts +16 -10
  70. package/dist/esm/query/query-builder.js +22 -29
  71. package/dist/esm/query/query-builder.js.map +1 -1
  72. package/dist/esm/query/schema.d.ts +12 -11
  73. package/dist/esm/query/select.d.ts +2 -2
  74. package/dist/esm/query/select.js +48 -25
  75. package/dist/esm/query/select.js.map +1 -1
  76. package/dist/esm/query/types.d.ts +1 -0
  77. package/dist/esm/transactions.js +20 -9
  78. package/dist/esm/transactions.js.map +1 -1
  79. package/dist/esm/types.d.ts +66 -7
  80. package/package.json +2 -2
  81. package/src/collection.ts +286 -146
  82. package/src/proxy.ts +141 -358
  83. package/src/query/compiled-query.ts +30 -15
  84. package/src/query/evaluators.ts +49 -21
  85. package/src/query/extractors.ts +24 -21
  86. package/src/query/group-by.ts +24 -22
  87. package/src/query/joins.ts +88 -75
  88. package/src/query/order-by.ts +56 -106
  89. package/src/query/pipeline-compiler.ts +34 -37
  90. package/src/query/query-builder.ts +49 -46
  91. package/src/query/schema.ts +18 -15
  92. package/src/query/select.ts +68 -33
  93. package/src/query/types.ts +1 -0
  94. package/src/transactions.ts +30 -14
  95. package/src/types.ts +76 -7
  96. package/dist/cjs/query/key-by.cjs +0 -43
  97. package/dist/cjs/query/key-by.cjs.map +0 -1
  98. package/dist/cjs/query/key-by.d.cts +0 -3
  99. package/dist/esm/query/key-by.d.ts +0 -3
  100. package/dist/esm/query/key-by.js +0 -43
  101. package/dist/esm/query/key-by.js.map +0 -1
  102. package/src/query/key-by.ts +0 -61
@@ -10,6 +10,7 @@ import type {
10
10
  OrderBy,
11
11
  Query,
12
12
  Select,
13
+ WhereCallback,
13
14
  WithQuery,
14
15
  } from "./schema.js"
15
16
  import type {
@@ -197,8 +198,9 @@ export class BaseQueryBuilder<TContext extends Context<Schema>> {
197
198
  /**
198
199
  * Specify what columns to select.
199
200
  * Overwrites any previous select clause.
201
+ * Also supports callback functions that receive the row context and return selected data.
200
202
  *
201
- * @param selects The columns to select
203
+ * @param selects The columns to select (can include callbacks)
202
204
  * @returns A new QueryBuilder with the select clause set
203
205
  */
204
206
  select<TSelects extends Array<Select<TContext>>>(
@@ -296,17 +298,23 @@ export class BaseQueryBuilder<TContext extends Context<Schema>> {
296
298
  */
297
299
  where(condition: Condition<TContext>): QueryBuilder<TContext>
298
300
 
301
+ /**
302
+ * Add a where clause with a callback function.
303
+ */
304
+ where(callback: WhereCallback<TContext>): QueryBuilder<TContext>
305
+
299
306
  /**
300
307
  * Add a where clause to filter the results.
301
308
  * Can be called multiple times to add AND conditions.
309
+ * Also supports callback functions that receive the row context.
302
310
  *
303
- * @param leftOrCondition The left operand or complete condition
311
+ * @param leftOrConditionOrCallback The left operand, complete condition, or callback function
304
312
  * @param operator Optional comparison operator
305
313
  * @param right Optional right operand
306
314
  * @returns A new QueryBuilder with the where clause added
307
315
  */
308
316
  where(
309
- leftOrCondition: any,
317
+ leftOrConditionOrCallback: any,
310
318
  operator?: any,
311
319
  right?: any
312
320
  ): QueryBuilder<TContext> {
@@ -317,22 +325,23 @@ export class BaseQueryBuilder<TContext extends Context<Schema>> {
317
325
 
318
326
  let condition: any
319
327
 
320
- // Determine if this is a complete condition or individual parts
321
- if (operator !== undefined && right !== undefined) {
328
+ // Determine if this is a callback, complete condition, or individual parts
329
+ if (typeof leftOrConditionOrCallback === `function`) {
330
+ // It's a callback function
331
+ condition = leftOrConditionOrCallback
332
+ } else if (operator !== undefined && right !== undefined) {
322
333
  // Create a condition from parts
323
- condition = [leftOrCondition, operator, right]
334
+ condition = [leftOrConditionOrCallback, operator, right]
324
335
  } else {
325
336
  // Use the provided condition directly
326
- condition = leftOrCondition
337
+ condition = leftOrConditionOrCallback
327
338
  }
328
339
 
340
+ // Where is always an array, so initialize or append
329
341
  if (!newBuilder.query.where) {
330
- newBuilder.query.where = condition
342
+ newBuilder.query.where = [condition]
331
343
  } else {
332
- // Create a composite condition with AND
333
- // Use any to bypass type checking issues
334
- const andArray: any = [newBuilder.query.where, `and`, condition]
335
- newBuilder.query.where = andArray
344
+ newBuilder.query.where = [...newBuilder.query.where, condition]
336
345
  }
337
346
 
338
347
  return newBuilder as unknown as QueryBuilder<TContext>
@@ -354,17 +363,24 @@ export class BaseQueryBuilder<TContext extends Context<Schema>> {
354
363
  */
355
364
  having(condition: Condition<TContext>): QueryBuilder<TContext>
356
365
 
366
+ /**
367
+ * Add a having clause with a callback function.
368
+ * For filtering results after they have been grouped.
369
+ */
370
+ having(callback: WhereCallback<TContext>): QueryBuilder<TContext>
371
+
357
372
  /**
358
373
  * Add a having clause to filter the grouped results.
359
374
  * Can be called multiple times to add AND conditions.
375
+ * Also supports callback functions that receive the row context.
360
376
  *
361
- * @param leftOrCondition The left operand or complete condition
377
+ * @param leftOrConditionOrCallback The left operand, complete condition, or callback function
362
378
  * @param operator Optional comparison operator
363
379
  * @param right Optional right operand
364
380
  * @returns A new QueryBuilder with the having clause added
365
381
  */
366
382
  having(
367
- leftOrCondition: any,
383
+ leftOrConditionOrCallback: any,
368
384
  operator?: any,
369
385
  right?: any
370
386
  ): QueryBuilder<TContext> {
@@ -374,22 +390,23 @@ export class BaseQueryBuilder<TContext extends Context<Schema>> {
374
390
 
375
391
  let condition: any
376
392
 
377
- // Determine if this is a complete condition or individual parts
378
- if (operator !== undefined && right !== undefined) {
393
+ // Determine if this is a callback, complete condition, or individual parts
394
+ if (typeof leftOrConditionOrCallback === `function`) {
395
+ // It's a callback function
396
+ condition = leftOrConditionOrCallback
397
+ } else if (operator !== undefined && right !== undefined) {
379
398
  // Create a condition from parts
380
- condition = [leftOrCondition, operator, right]
399
+ condition = [leftOrConditionOrCallback, operator, right]
381
400
  } else {
382
401
  // Use the provided condition directly
383
- condition = leftOrCondition
402
+ condition = leftOrConditionOrCallback
384
403
  }
385
404
 
405
+ // Having is always an array, so initialize or append
386
406
  if (!newBuilder.query.having) {
387
- newBuilder.query.having = condition
407
+ newBuilder.query.having = [condition]
388
408
  } else {
389
- // Create a composite condition with AND
390
- // Use any to bypass type checking issues
391
- const andArray: any = [newBuilder.query.having, `and`, condition]
392
- newBuilder.query.having = andArray
409
+ newBuilder.query.having = [...newBuilder.query.having, condition]
393
410
  }
394
411
 
395
412
  return newBuilder as QueryBuilder<TContext>
@@ -438,6 +455,7 @@ export class BaseQueryBuilder<TContext extends Context<Schema>> {
438
455
  Input
439
456
  >
440
457
  }
458
+ hasJoin: true
441
459
  }
442
460
  >
443
461
  >
@@ -474,6 +492,7 @@ export class BaseQueryBuilder<TContext extends Context<Schema>> {
474
492
  schema: TContext[`schema`] & {
475
493
  [K in T]: RemoveIndexSignature<TContext[`baseSchema`][T]>
476
494
  }
495
+ hasJoin: true
477
496
  }
478
497
  >
479
498
  >
@@ -513,6 +532,7 @@ export class BaseQueryBuilder<TContext extends Context<Schema>> {
513
532
  schema: TContext[`schema`] & {
514
533
  [K in TAs]: RemoveIndexSignature<TContext[`baseSchema`][TFrom]>
515
534
  }
535
+ hasJoin: true
516
536
  }
517
537
  >
518
538
  >
@@ -754,25 +774,6 @@ export class BaseQueryBuilder<TContext extends Context<Schema>> {
754
774
  return newBuilder as QueryBuilder<TContext>
755
775
  }
756
776
 
757
- /**
758
- * Specify which column(s) to use as keys in the output keyed stream.
759
- *
760
- * @param keyBy The column(s) to use as keys
761
- * @returns A new QueryBuilder with the keyBy clause set
762
- */
763
- keyBy(
764
- keyBy: PropertyReference<TContext> | Array<PropertyReference<TContext>>
765
- ): QueryBuilder<TContext> {
766
- // Create a new builder with a copy of the current query
767
- const newBuilder = new BaseQueryBuilder<TContext>()
768
- Object.assign(newBuilder.query, this.query)
769
-
770
- // Set the keyBy clause
771
- newBuilder.query.keyBy = keyBy
772
-
773
- return newBuilder as QueryBuilder<TContext>
774
- }
775
-
776
777
  /**
777
778
  * Add a groupBy clause to group the results by one or more columns.
778
779
  *
@@ -883,10 +884,12 @@ export function queryBuilder<TBaseSchema extends Schema = {}>() {
883
884
 
884
885
  export type ResultsFromContext<TContext extends Context<Schema>> = Flatten<
885
886
  TContext[`result`] extends object
886
- ? TContext[`result`]
887
- : TContext[`result`] extends undefined
888
- ? TContext[`schema`]
889
- : object
887
+ ? TContext[`result`] // If there is a select we will have a result type
888
+ : TContext[`hasJoin`] extends true
889
+ ? TContext[`schema`] // If there is a join, the query returns the namespaced schema
890
+ : TContext[`default`] extends keyof TContext[`schema`]
891
+ ? TContext[`schema`][TContext[`default`]] // If there is no join we return the flat default schema
892
+ : never // Should never happen
890
893
  >
891
894
 
892
895
  export type ResultFromQueryBuilder<TQueryBuilder> = Flatten<
@@ -169,7 +169,6 @@ export interface JoinClause<TContext extends Context = Context> {
169
169
  from: string
170
170
  as?: string
171
171
  on: Condition<TContext>
172
- where?: Condition<TContext>
173
172
  }
174
173
 
175
174
  // The orderBy clause can be a string, an object mapping a column to "asc" or "desc",
@@ -192,6 +191,11 @@ export type Select<TContext extends Context = Context> =
192
191
  | AggregateFunctionCall<TContext>
193
192
  }
194
193
  | WildcardReferenceString<TContext>
194
+ | SelectCallback<TContext>
195
+
196
+ export type SelectCallback<TContext extends Context = Context> = (
197
+ context: TContext extends { schema: infer S } ? S : any
198
+ ) => any
195
199
 
196
200
  export type As<TContext extends Context = Context> = string
197
201
 
@@ -200,14 +204,21 @@ export type From<TContext extends Context = Context> = InputReference<{
200
204
  schema: TContext[`baseSchema`]
201
205
  }>
202
206
 
203
- export type Where<TContext extends Context = Context> = Condition<TContext>
207
+ export type WhereCallback<TContext extends Context = Context> = (
208
+ context: TContext extends { schema: infer S } ? S : any
209
+ ) => boolean
210
+
211
+ export type Where<TContext extends Context = Context> = Array<
212
+ Condition<TContext> | WhereCallback<TContext>
213
+ >
214
+
215
+ // Having is the same implementation as a where clause, its just run after the group by
216
+ export type Having<TContext extends Context = Context> = Where<TContext>
204
217
 
205
218
  export type GroupBy<TContext extends Context = Context> =
206
219
  | PropertyReference<TContext>
207
220
  | Array<PropertyReference<TContext>>
208
221
 
209
- export type Having<TContext extends Context = Context> = Condition<TContext>
210
-
211
222
  export type Limit<TContext extends Context = Context> = number
212
223
 
213
224
  export type Offset<TContext extends Context = Context> = number
@@ -217,13 +228,13 @@ export interface BaseQuery<TContext extends Context = Context> {
217
228
  // to expressions. Plain strings starting with "@" denote column references.
218
229
  // Plain string "@*" denotes all columns from all tables.
219
230
  // Plain string "@table.*" denotes all columns from a specific table.
220
- select: Array<Select<TContext>>
231
+ select?: Array<Select<TContext>>
221
232
  as?: As<TContext>
222
233
  from: From<TContext>
223
234
  join?: Array<JoinClause<TContext>>
224
- where?: Condition<TContext>
235
+ where?: Where<TContext>
225
236
  groupBy?: GroupBy<TContext>
226
- having?: Condition<TContext>
237
+ having?: Having<TContext>
227
238
  orderBy?: OrderBy<TContext>
228
239
  limit?: Limit<TContext>
229
240
  offset?: Offset<TContext>
@@ -232,7 +243,6 @@ export interface BaseQuery<TContext extends Context = Context> {
232
243
  // The top-level query interface.
233
244
  export interface Query<TContext extends Context = Context>
234
245
  extends BaseQuery<TContext> {
235
- keyBy?: PropertyReference<TContext> | Array<PropertyReference<TContext>>
236
246
  with?: Array<WithQuery<TContext>>
237
247
  collections?: {
238
248
  [K: string]: Collection<any>
@@ -246,10 +256,3 @@ export interface WithQuery<TContext extends Context = Context>
246
256
  extends BaseQuery<TContext> {
247
257
  as: string
248
258
  }
249
-
250
- // A keyed query is a query that has a keyBy clause, and so the result is always
251
- // a keyed stream.
252
- export interface KeyedQuery<TContext extends Context = Context>
253
- extends Query<TContext> {
254
- keyBy: PropertyReference<TContext> | Array<PropertyReference<TContext>>
255
- }
@@ -1,41 +1,71 @@
1
1
  import { map } from "@electric-sql/d2ts"
2
2
  import {
3
- evaluateOperandOnNestedRow,
4
- extractValueFromNestedRow,
3
+ evaluateOperandOnNamespacedRow,
4
+ extractValueFromNamespacedRow,
5
5
  } from "./extractors"
6
- import type { IStreamBuilder } from "@electric-sql/d2ts"
7
- import type { ConditionOperand, Query } from "./schema"
6
+ import type { ConditionOperand, Query, SelectCallback } from "./schema"
7
+ import type { KeyedStream, NamespacedAndKeyedStream } from "../types"
8
8
 
9
9
  export function processSelect(
10
- pipeline: IStreamBuilder<Record<string, unknown>>,
10
+ pipeline: NamespacedAndKeyedStream,
11
11
  query: Query,
12
12
  mainTableAlias: string,
13
- inputs: Record<string, IStreamBuilder<Record<string, unknown>>>
14
- ) {
13
+ inputs: Record<string, KeyedStream>
14
+ ): KeyedStream {
15
15
  return pipeline.pipe(
16
- map((nestedRow: Record<string, unknown>) => {
16
+ map(([key, namespacedRow]) => {
17
17
  const result: Record<string, unknown> = {}
18
18
 
19
19
  // Check if this is a grouped result (has no nested table structure)
20
20
  // If it's a grouped result, we need to handle it differently
21
21
  const isGroupedResult =
22
22
  query.groupBy &&
23
- Object.keys(nestedRow).some(
24
- (key) =>
25
- !Object.keys(inputs).includes(key) &&
26
- typeof nestedRow[key] !== `object`
23
+ Object.keys(namespacedRow).some(
24
+ (namespaceKey) =>
25
+ !Object.keys(inputs).includes(namespaceKey) &&
26
+ typeof namespacedRow[namespaceKey] !== `object`
27
27
  )
28
28
 
29
+ if (!query.select) {
30
+ throw new Error(`Cannot process missing SELECT clause`)
31
+ }
32
+
29
33
  for (const item of query.select) {
34
+ // Handle callback functions
35
+ if (typeof item === `function`) {
36
+ const callback = item as SelectCallback
37
+ const callbackResult = callback(namespacedRow)
38
+
39
+ // If the callback returns an object, merge its properties into the result
40
+ if (
41
+ callbackResult &&
42
+ typeof callbackResult === `object` &&
43
+ !Array.isArray(callbackResult)
44
+ ) {
45
+ Object.assign(result, callbackResult)
46
+ } else {
47
+ // If the callback returns a primitive value, we can't merge it
48
+ // This would need a specific key, but since we don't have one, we'll skip it
49
+ // In practice, select callbacks should return objects with keys
50
+ console.warn(
51
+ `SelectCallback returned a non-object value. SelectCallbacks should return objects with key-value pairs.`
52
+ )
53
+ }
54
+ continue
55
+ }
56
+
30
57
  if (typeof item === `string`) {
31
58
  // Handle wildcard select - all columns from all tables
32
59
  if ((item as string) === `@*`) {
33
60
  // For grouped results, just return the row as is
34
61
  if (isGroupedResult) {
35
- Object.assign(result, nestedRow)
62
+ Object.assign(result, namespacedRow)
36
63
  } else {
37
64
  // Extract all columns from all tables
38
- Object.assign(result, extractAllColumnsFromAllTables(nestedRow))
65
+ Object.assign(
66
+ result,
67
+ extractAllColumnsFromAllTables(namespacedRow)
68
+ )
39
69
  }
40
70
  continue
41
71
  }
@@ -56,7 +86,7 @@ export function processSelect(
56
86
  // Extract all columns from the specified table
57
87
  Object.assign(
58
88
  result,
59
- extractAllColumnsFromTable(nestedRow, tableAlias)
89
+ extractAllColumnsFromTable(namespacedRow, tableAlias)
60
90
  )
61
91
  }
62
92
  continue
@@ -68,12 +98,12 @@ export function processSelect(
68
98
  const alias = columnRef
69
99
 
70
100
  // For grouped results, check if the column is directly in the row first
71
- if (isGroupedResult && columnRef in nestedRow) {
72
- result[alias] = nestedRow[columnRef]
101
+ if (isGroupedResult && columnRef in namespacedRow) {
102
+ result[alias] = namespacedRow[columnRef]
73
103
  } else {
74
104
  // Extract the value from the nested structure
75
- result[alias] = extractValueFromNestedRow(
76
- nestedRow,
105
+ result[alias] = extractValueFromNamespacedRow(
106
+ namespacedRow,
77
107
  columnRef,
78
108
  mainTableAlias,
79
109
  undefined
@@ -95,12 +125,12 @@ export function processSelect(
95
125
  const columnRef = (expr as string).substring(1)
96
126
 
97
127
  // For grouped results, check if the column is directly in the row first
98
- if (isGroupedResult && columnRef in nestedRow) {
99
- result[alias] = nestedRow[columnRef]
128
+ if (isGroupedResult && columnRef in namespacedRow) {
129
+ result[alias] = namespacedRow[columnRef]
100
130
  } else {
101
131
  // Extract the value from the nested structure
102
- result[alias] = extractValueFromNestedRow(
103
- nestedRow,
132
+ result[alias] = extractValueFromNamespacedRow(
133
+ namespacedRow,
104
134
  columnRef,
105
135
  mainTableAlias,
106
136
  undefined
@@ -108,12 +138,14 @@ export function processSelect(
108
138
  }
109
139
  } else if (typeof expr === `object`) {
110
140
  // For grouped results, the aggregate results are already in the row
111
- if (isGroupedResult && alias in nestedRow) {
112
- result[alias] = nestedRow[alias]
141
+ if (isGroupedResult && alias in namespacedRow) {
142
+ result[alias] = namespacedRow[alias]
143
+ } else if ((expr as { ORDER_INDEX: unknown }).ORDER_INDEX) {
144
+ result[alias] = namespacedRow[mainTableAlias]![alias]
113
145
  } else {
114
146
  // This might be a function call
115
- result[alias] = evaluateOperandOnNestedRow(
116
- nestedRow,
147
+ result[alias] = evaluateOperandOnNamespacedRow(
148
+ namespacedRow,
117
149
  expr as ConditionOperand,
118
150
  mainTableAlias,
119
151
  undefined
@@ -124,23 +156,26 @@ export function processSelect(
124
156
  }
125
157
  }
126
158
 
127
- return result
159
+ return [key, result] as [string, typeof result]
128
160
  })
129
161
  )
130
162
  }
131
163
 
132
164
  // Helper function to extract all columns from all tables in a nested row
133
165
  function extractAllColumnsFromAllTables(
134
- nestedRow: Record<string, unknown>
166
+ namespacedRow: Record<string, unknown>
135
167
  ): Record<string, unknown> {
136
168
  const result: Record<string, unknown> = {}
137
169
 
138
170
  // Process each table in the nested row
139
- for (const [tableAlias, tableData] of Object.entries(nestedRow)) {
171
+ for (const [tableAlias, tableData] of Object.entries(namespacedRow)) {
140
172
  if (tableData && typeof tableData === `object`) {
141
173
  // Add all columns from this table to the result
142
174
  // If there are column name conflicts, the last table's columns will overwrite previous ones
143
- Object.assign(result, extractAllColumnsFromTable(nestedRow, tableAlias))
175
+ Object.assign(
176
+ result,
177
+ extractAllColumnsFromTable(namespacedRow, tableAlias)
178
+ )
144
179
  }
145
180
  }
146
181
 
@@ -149,13 +184,13 @@ function extractAllColumnsFromAllTables(
149
184
 
150
185
  // Helper function to extract all columns from a table in a nested row
151
186
  function extractAllColumnsFromTable(
152
- nestedRow: Record<string, unknown>,
187
+ namespacedRow: Record<string, unknown>,
153
188
  tableAlias: string
154
189
  ): Record<string, unknown> {
155
190
  const result: Record<string, unknown> = {}
156
191
 
157
192
  // Get the table data
158
- const tableData = nestedRow[tableAlias] as
193
+ const tableData = namespacedRow[tableAlias] as
159
194
  | Record<string, unknown>
160
195
  | null
161
196
  | undefined
@@ -20,6 +20,7 @@ export type Context<
20
20
  schema: TSchema
21
21
  default?: keyof TSchema
22
22
  result?: Record<string, unknown>
23
+ hasJoin?: boolean
23
24
  }
24
25
 
25
26
  // Helper types
@@ -4,6 +4,7 @@ import type {
4
4
  PendingMutation,
5
5
  TransactionConfig,
6
6
  TransactionState,
7
+ TransactionWithMutations,
7
8
  } from "./types"
8
9
 
9
10
  function generateUUID() {
@@ -24,6 +25,7 @@ function generateUUID() {
24
25
  }
25
26
 
26
27
  const transactions: Array<Transaction> = []
28
+ let transactionStack: Array<Transaction> = []
27
29
 
28
30
  export function createTransaction(config: TransactionConfig): Transaction {
29
31
  if (typeof config.mutationFn === `undefined`) {
@@ -35,13 +37,12 @@ export function createTransaction(config: TransactionConfig): Transaction {
35
37
  transactionId = generateUUID()
36
38
  }
37
39
  const newTransaction = new Transaction({ ...config, id: transactionId })
40
+
38
41
  transactions.push(newTransaction)
39
42
 
40
43
  return newTransaction
41
44
  }
42
45
 
43
- let transactionStack: Array<Transaction> = []
44
-
45
46
  export function getActiveTransaction(): Transaction | undefined {
46
47
  if (transactionStack.length > 0) {
47
48
  return transactionStack.slice(-1)[0]
@@ -58,6 +59,13 @@ function unregisterTransaction(tx: Transaction) {
58
59
  transactionStack = transactionStack.filter((t) => t.id !== tx.id)
59
60
  }
60
61
 
62
+ function removeFromPendingList(tx: Transaction) {
63
+ const index = transactions.findIndex((t) => t.id === tx.id)
64
+ if (index !== -1) {
65
+ transactions.splice(index, 1)
66
+ }
67
+ }
68
+
61
69
  export class Transaction {
62
70
  public id: string
63
71
  public state: TransactionState
@@ -85,6 +93,10 @@ export class Transaction {
85
93
 
86
94
  setState(newState: TransactionState) {
87
95
  this.state = newState
96
+
97
+ if (newState === `completed` || newState === `failed`) {
98
+ removeFromPendingList(this)
99
+ }
88
100
  }
89
101
 
90
102
  mutate(callback: () => void): Transaction {
@@ -130,22 +142,20 @@ export class Transaction {
130
142
 
131
143
  this.setState(`failed`)
132
144
 
133
- // See if there's any other transactions w/ mutations on the same keys
145
+ // See if there's any other transactions w/ mutations on the same ids
134
146
  // and roll them back as well.
135
147
  if (!isSecondaryRollback) {
136
- const mutationKeys = new Set()
137
- this.mutations.forEach((m) => mutationKeys.add(m.key))
138
- transactions.forEach(
139
- (t) =>
140
- t.state === `pending` &&
141
- t.mutations.some((m) => mutationKeys.has(m.key)) &&
148
+ const mutationIds = new Set()
149
+ this.mutations.forEach((m) => mutationIds.add(m.key))
150
+ for (const t of transactions) {
151
+ t.state === `pending` &&
152
+ t.mutations.some((m) => mutationIds.has(m.key)) &&
142
153
  t.rollback({ isSecondaryRollback: true })
143
- )
154
+ }
144
155
  }
145
156
 
146
157
  // Reject the promise
147
158
  this.isPersisted.reject(this.error?.error)
148
-
149
159
  this.touchCollection()
150
160
 
151
161
  return this
@@ -154,13 +164,13 @@ export class Transaction {
154
164
  // Tell collection that something has changed with the transaction
155
165
  touchCollection(): void {
156
166
  const hasCalled = new Set()
157
- this.mutations.forEach((mutation) => {
167
+ for (const mutation of this.mutations) {
158
168
  if (!hasCalled.has(mutation.collection.id)) {
159
169
  mutation.collection.transactions.setState((state) => state)
160
170
  mutation.collection.commitPendingTransactions()
161
171
  hasCalled.add(mutation.collection.id)
162
172
  }
163
- })
173
+ }
164
174
  }
165
175
 
166
176
  async commit(): Promise<Transaction> {
@@ -172,11 +182,17 @@ export class Transaction {
172
182
 
173
183
  if (this.mutations.length === 0) {
174
184
  this.setState(`completed`)
185
+
186
+ return this
175
187
  }
176
188
 
177
189
  // Run mutationFn
178
190
  try {
179
- await this.mutationFn({ transaction: this })
191
+ // At this point we know there's at least one mutation
192
+ // Use type assertion to tell TypeScript about this guarantee
193
+ const transactionWithMutations =
194
+ this as unknown as TransactionWithMutations
195
+ await this.mutationFn({ transaction: transactionWithMutations })
180
196
 
181
197
  this.setState(`completed`)
182
198
  this.touchCollection()