@tanstack/db 0.0.5 → 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.
- package/dist/cjs/collection.cjs +71 -21
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +8 -7
- package/dist/cjs/query/evaluators.cjs +15 -0
- package/dist/cjs/query/evaluators.cjs.map +1 -1
- package/dist/cjs/query/evaluators.d.cts +5 -1
- package/dist/cjs/query/pipeline-compiler.cjs +2 -2
- package/dist/cjs/query/pipeline-compiler.cjs.map +1 -1
- package/dist/cjs/query/query-builder.cjs +22 -17
- package/dist/cjs/query/query-builder.cjs.map +1 -1
- package/dist/cjs/query/query-builder.d.cts +12 -2
- package/dist/cjs/query/schema.d.cts +11 -5
- package/dist/cjs/query/select.cjs +12 -0
- package/dist/cjs/query/select.cjs.map +1 -1
- package/dist/cjs/transactions.cjs +3 -1
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/types.d.cts +25 -0
- package/dist/esm/collection.d.ts +8 -7
- package/dist/esm/collection.js +72 -22
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/query/evaluators.d.ts +5 -1
- package/dist/esm/query/evaluators.js +16 -1
- package/dist/esm/query/evaluators.js.map +1 -1
- package/dist/esm/query/pipeline-compiler.js +3 -3
- package/dist/esm/query/pipeline-compiler.js.map +1 -1
- package/dist/esm/query/query-builder.d.ts +12 -2
- package/dist/esm/query/query-builder.js +22 -17
- package/dist/esm/query/query-builder.js.map +1 -1
- package/dist/esm/query/schema.d.ts +11 -5
- package/dist/esm/query/select.js +12 -0
- package/dist/esm/query/select.js.map +1 -1
- package/dist/esm/transactions.js +3 -1
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +25 -0
- package/package.json +1 -1
- package/src/collection.ts +117 -30
- package/src/query/evaluators.ts +27 -0
- package/src/query/pipeline-compiler.ts +3 -3
- package/src/query/query-builder.ts +40 -23
- package/src/query/schema.ts +17 -5
- package/src/query/select.ts +24 -1
- package/src/transactions.ts +8 -1
- package/src/types.ts +28 -0
package/src/collection.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Derived, Store, batch } from "@tanstack/store"
|
|
2
2
|
import { withArrayChangeTracking, withChangeTracking } from "./proxy"
|
|
3
|
-
import { getActiveTransaction } from "./transactions"
|
|
3
|
+
import { Transaction, getActiveTransaction } from "./transactions"
|
|
4
4
|
import { SortedMap } from "./SortedMap"
|
|
5
5
|
import type {
|
|
6
6
|
ChangeMessage,
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
OptimisticChangeMessage,
|
|
11
11
|
PendingMutation,
|
|
12
12
|
StandardSchema,
|
|
13
|
-
Transaction,
|
|
13
|
+
Transaction as TransactionType,
|
|
14
14
|
} from "./types"
|
|
15
15
|
|
|
16
16
|
// Store collections in memory using Tanstack store
|
|
@@ -169,7 +169,7 @@ export class SchemaValidationError extends Error {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
export class Collection<T extends object = Record<string, unknown>> {
|
|
172
|
-
public transactions: Store<SortedMap<string,
|
|
172
|
+
public transactions: Store<SortedMap<string, TransactionType>>
|
|
173
173
|
public optimisticOperations: Derived<Array<OptimisticChangeMessage<T>>>
|
|
174
174
|
public derivedState: Derived<Map<string, T>>
|
|
175
175
|
public derivedArray: Derived<Array<T>>
|
|
@@ -218,7 +218,7 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
this.transactions = new Store(
|
|
221
|
-
new SortedMap<string,
|
|
221
|
+
new SortedMap<string, TransactionType>(
|
|
222
222
|
(a, b) => a.createdAt.getTime() - b.createdAt.getTime()
|
|
223
223
|
)
|
|
224
224
|
)
|
|
@@ -604,7 +604,7 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
604
604
|
* Inserts one or more items into the collection
|
|
605
605
|
* @param items - Single item or array of items to insert
|
|
606
606
|
* @param config - Optional configuration including metadata and custom keys
|
|
607
|
-
* @returns A
|
|
607
|
+
* @returns A TransactionType object representing the insert operation(s)
|
|
608
608
|
* @throws {SchemaValidationError} If the data fails schema validation
|
|
609
609
|
* @example
|
|
610
610
|
* // Insert a single item
|
|
@@ -620,9 +620,13 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
620
620
|
* insert({ text: "Buy groceries" }, { key: "grocery-task" })
|
|
621
621
|
*/
|
|
622
622
|
insert = (data: T | Array<T>, config?: InsertConfig) => {
|
|
623
|
-
const
|
|
624
|
-
|
|
625
|
-
|
|
623
|
+
const ambientTransaction = getActiveTransaction()
|
|
624
|
+
|
|
625
|
+
// If no ambient transaction exists, check for an onInsert handler early
|
|
626
|
+
if (!ambientTransaction && !this.config.onInsert) {
|
|
627
|
+
throw new Error(
|
|
628
|
+
`Collection.insert called directly (not within an explicit transaction) but no 'onInsert' handler is configured.`
|
|
629
|
+
)
|
|
626
630
|
}
|
|
627
631
|
|
|
628
632
|
const items = Array.isArray(data) ? data : [data]
|
|
@@ -662,14 +666,37 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
662
666
|
mutations.push(mutation)
|
|
663
667
|
})
|
|
664
668
|
|
|
665
|
-
transaction
|
|
669
|
+
// If an ambient transaction exists, use it
|
|
670
|
+
if (ambientTransaction) {
|
|
671
|
+
ambientTransaction.applyMutations(mutations)
|
|
666
672
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
673
|
+
this.transactions.setState((sortedMap) => {
|
|
674
|
+
sortedMap.set(ambientTransaction.id, ambientTransaction)
|
|
675
|
+
return sortedMap
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
return ambientTransaction
|
|
679
|
+
} else {
|
|
680
|
+
// Create a new transaction with a mutation function that calls the onInsert handler
|
|
681
|
+
const directOpTransaction = new Transaction({
|
|
682
|
+
mutationFn: async (params) => {
|
|
683
|
+
// Call the onInsert handler with the transaction
|
|
684
|
+
return this.config.onInsert!(params)
|
|
685
|
+
},
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
// Apply mutations to the new transaction
|
|
689
|
+
directOpTransaction.applyMutations(mutations)
|
|
690
|
+
directOpTransaction.commit()
|
|
671
691
|
|
|
672
|
-
|
|
692
|
+
// Add the transaction to the collection's transactions store
|
|
693
|
+
this.transactions.setState((sortedMap) => {
|
|
694
|
+
sortedMap.set(directOpTransaction.id, directOpTransaction)
|
|
695
|
+
return sortedMap
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
return directOpTransaction
|
|
699
|
+
}
|
|
673
700
|
}
|
|
674
701
|
|
|
675
702
|
/**
|
|
@@ -715,13 +742,13 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
715
742
|
id: unknown,
|
|
716
743
|
configOrCallback: ((draft: TItem) => void) | OperationConfig,
|
|
717
744
|
maybeCallback?: (draft: TItem) => void
|
|
718
|
-
):
|
|
745
|
+
): TransactionType
|
|
719
746
|
|
|
720
747
|
update<TItem extends object = T>(
|
|
721
748
|
ids: Array<unknown>,
|
|
722
749
|
configOrCallback: ((draft: Array<TItem>) => void) | OperationConfig,
|
|
723
750
|
maybeCallback?: (draft: Array<TItem>) => void
|
|
724
|
-
):
|
|
751
|
+
): TransactionType
|
|
725
752
|
|
|
726
753
|
update<TItem extends object = T>(
|
|
727
754
|
ids: unknown | Array<unknown>,
|
|
@@ -732,9 +759,13 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
732
759
|
throw new Error(`The first argument to update is missing`)
|
|
733
760
|
}
|
|
734
761
|
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
762
|
+
const ambientTransaction = getActiveTransaction()
|
|
763
|
+
|
|
764
|
+
// If no ambient transaction exists, check for an onUpdate handler early
|
|
765
|
+
if (!ambientTransaction && !this.config.onUpdate) {
|
|
766
|
+
throw new Error(
|
|
767
|
+
`Collection.update called directly (not within an explicit transaction) but no 'onUpdate' handler is configured.`
|
|
768
|
+
)
|
|
738
769
|
}
|
|
739
770
|
|
|
740
771
|
const isArray = Array.isArray(ids)
|
|
@@ -828,21 +859,46 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
828
859
|
throw new Error(`No changes were made to any of the objects`)
|
|
829
860
|
}
|
|
830
861
|
|
|
831
|
-
transaction
|
|
862
|
+
// If an ambient transaction exists, use it
|
|
863
|
+
if (ambientTransaction) {
|
|
864
|
+
ambientTransaction.applyMutations(mutations)
|
|
865
|
+
|
|
866
|
+
this.transactions.setState((sortedMap) => {
|
|
867
|
+
sortedMap.set(ambientTransaction.id, ambientTransaction)
|
|
868
|
+
return sortedMap
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
return ambientTransaction
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// No need to check for onUpdate handler here as we've already checked at the beginning
|
|
832
875
|
|
|
876
|
+
// Create a new transaction with a mutation function that calls the onUpdate handler
|
|
877
|
+
const directOpTransaction = new Transaction({
|
|
878
|
+
mutationFn: async (transaction) => {
|
|
879
|
+
// Call the onUpdate handler with the transaction
|
|
880
|
+
return this.config.onUpdate!(transaction)
|
|
881
|
+
},
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
// Apply mutations to the new transaction
|
|
885
|
+
directOpTransaction.applyMutations(mutations)
|
|
886
|
+
directOpTransaction.commit()
|
|
887
|
+
|
|
888
|
+
// Add the transaction to the collection's transactions store
|
|
833
889
|
this.transactions.setState((sortedMap) => {
|
|
834
|
-
sortedMap.set(
|
|
890
|
+
sortedMap.set(directOpTransaction.id, directOpTransaction)
|
|
835
891
|
return sortedMap
|
|
836
892
|
})
|
|
837
893
|
|
|
838
|
-
return
|
|
894
|
+
return directOpTransaction
|
|
839
895
|
}
|
|
840
896
|
|
|
841
897
|
/**
|
|
842
898
|
* Deletes one or more items from the collection
|
|
843
899
|
* @param ids - Single ID or array of IDs to delete
|
|
844
900
|
* @param config - Optional configuration including metadata
|
|
845
|
-
* @returns A
|
|
901
|
+
* @returns A TransactionType object representing the delete operation(s)
|
|
846
902
|
* @example
|
|
847
903
|
* // Delete a single item
|
|
848
904
|
* delete("todo-1")
|
|
@@ -853,10 +909,17 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
853
909
|
* // Delete with metadata
|
|
854
910
|
* delete("todo-1", { metadata: { reason: "completed" } })
|
|
855
911
|
*/
|
|
856
|
-
delete = (
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
912
|
+
delete = (
|
|
913
|
+
ids: Array<string> | string,
|
|
914
|
+
config?: OperationConfig
|
|
915
|
+
): TransactionType => {
|
|
916
|
+
const ambientTransaction = getActiveTransaction()
|
|
917
|
+
|
|
918
|
+
// If no ambient transaction exists, check for an onDelete handler early
|
|
919
|
+
if (!ambientTransaction && !this.config.onDelete) {
|
|
920
|
+
throw new Error(
|
|
921
|
+
`Collection.delete called directly (not within an explicit transaction) but no 'onDelete' handler is configured.`
|
|
922
|
+
)
|
|
860
923
|
}
|
|
861
924
|
|
|
862
925
|
const idsArray = (Array.isArray(ids) ? ids : [ids]).map((id) =>
|
|
@@ -885,14 +948,38 @@ export class Collection<T extends object = Record<string, unknown>> {
|
|
|
885
948
|
mutations.push(mutation)
|
|
886
949
|
}
|
|
887
950
|
|
|
888
|
-
transaction
|
|
951
|
+
// If an ambient transaction exists, use it
|
|
952
|
+
if (ambientTransaction) {
|
|
953
|
+
ambientTransaction.applyMutations(mutations)
|
|
954
|
+
|
|
955
|
+
this.transactions.setState((sortedMap) => {
|
|
956
|
+
sortedMap.set(ambientTransaction.id, ambientTransaction)
|
|
957
|
+
return sortedMap
|
|
958
|
+
})
|
|
959
|
+
|
|
960
|
+
return ambientTransaction
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Create a new transaction with a mutation function that calls the onDelete handler
|
|
964
|
+
const directOpTransaction = new Transaction({
|
|
965
|
+
autoCommit: true,
|
|
966
|
+
mutationFn: async (transaction) => {
|
|
967
|
+
// Call the onDelete handler with the transaction
|
|
968
|
+
return this.config.onDelete!(transaction)
|
|
969
|
+
},
|
|
970
|
+
})
|
|
971
|
+
|
|
972
|
+
// Apply mutations to the new transaction
|
|
973
|
+
directOpTransaction.applyMutations(mutations)
|
|
974
|
+
directOpTransaction.commit()
|
|
889
975
|
|
|
976
|
+
// Add the transaction to the collection's transactions store
|
|
890
977
|
this.transactions.setState((sortedMap) => {
|
|
891
|
-
sortedMap.set(
|
|
978
|
+
sortedMap.set(directOpTransaction.id, directOpTransaction)
|
|
892
979
|
return sortedMap
|
|
893
980
|
})
|
|
894
981
|
|
|
895
|
-
return
|
|
982
|
+
return directOpTransaction
|
|
896
983
|
}
|
|
897
984
|
|
|
898
985
|
/**
|
package/src/query/evaluators.ts
CHANGED
|
@@ -6,9 +6,36 @@ import type {
|
|
|
6
6
|
ConditionOperand,
|
|
7
7
|
LogicalOperator,
|
|
8
8
|
SimpleCondition,
|
|
9
|
+
Where,
|
|
10
|
+
WhereCallback,
|
|
9
11
|
} from "./schema.js"
|
|
10
12
|
import type { NamespacedRow } from "../types.js"
|
|
11
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Evaluates a Where clause (which is always an array of conditions and/or callbacks) against a nested row structure
|
|
16
|
+
*/
|
|
17
|
+
export function evaluateWhereOnNamespacedRow(
|
|
18
|
+
namespacedRow: NamespacedRow,
|
|
19
|
+
where: Where,
|
|
20
|
+
mainTableAlias?: string,
|
|
21
|
+
joinedTableAlias?: string
|
|
22
|
+
): boolean {
|
|
23
|
+
// Where is always an array of conditions and/or callbacks
|
|
24
|
+
// Evaluate all items and combine with AND logic
|
|
25
|
+
return where.every((item) => {
|
|
26
|
+
if (typeof item === `function`) {
|
|
27
|
+
return (item as WhereCallback)(namespacedRow)
|
|
28
|
+
} else {
|
|
29
|
+
return evaluateConditionOnNamespacedRow(
|
|
30
|
+
namespacedRow,
|
|
31
|
+
item as Condition,
|
|
32
|
+
mainTableAlias,
|
|
33
|
+
joinedTableAlias
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
12
39
|
/**
|
|
13
40
|
* Evaluates a condition against a nested row structure
|
|
14
41
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { filter, map } from "@electric-sql/d2ts"
|
|
2
|
-
import {
|
|
2
|
+
import { evaluateWhereOnNamespacedRow } from "./evaluators.js"
|
|
3
3
|
import { processJoinClause } from "./joins.js"
|
|
4
4
|
import { processGroupBy } from "./group-by.js"
|
|
5
5
|
import { processOrderBy } from "./order-by.js"
|
|
@@ -95,7 +95,7 @@ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
|
|
|
95
95
|
if (query.where) {
|
|
96
96
|
pipeline = pipeline.pipe(
|
|
97
97
|
filter(([_key, row]) => {
|
|
98
|
-
const result =
|
|
98
|
+
const result = evaluateWhereOnNamespacedRow(
|
|
99
99
|
row,
|
|
100
100
|
query.where!,
|
|
101
101
|
mainTableAlias
|
|
@@ -117,7 +117,7 @@ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
|
|
|
117
117
|
filter(([_key, row]) => {
|
|
118
118
|
// For HAVING, we're working with the flattened row that contains both
|
|
119
119
|
// the group by keys and the aggregate results directly
|
|
120
|
-
const result =
|
|
120
|
+
const result = evaluateWhereOnNamespacedRow(
|
|
121
121
|
row,
|
|
122
122
|
query.having!,
|
|
123
123
|
mainTableAlias
|
|
@@ -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
|
|
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
|
-
|
|
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 (
|
|
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 = [
|
|
334
|
+
condition = [leftOrConditionOrCallback, operator, right]
|
|
324
335
|
} else {
|
|
325
336
|
// Use the provided condition directly
|
|
326
|
-
condition =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 = [
|
|
399
|
+
condition = [leftOrConditionOrCallback, operator, right]
|
|
381
400
|
} else {
|
|
382
401
|
// Use the provided condition directly
|
|
383
|
-
condition =
|
|
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
|
-
|
|
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>
|
package/src/query/schema.ts
CHANGED
|
@@ -191,6 +191,11 @@ export type Select<TContext extends Context = Context> =
|
|
|
191
191
|
| AggregateFunctionCall<TContext>
|
|
192
192
|
}
|
|
193
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
|
|
194
199
|
|
|
195
200
|
export type As<TContext extends Context = Context> = string
|
|
196
201
|
|
|
@@ -199,14 +204,21 @@ export type From<TContext extends Context = Context> = InputReference<{
|
|
|
199
204
|
schema: TContext[`baseSchema`]
|
|
200
205
|
}>
|
|
201
206
|
|
|
202
|
-
export type
|
|
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>
|
|
203
217
|
|
|
204
218
|
export type GroupBy<TContext extends Context = Context> =
|
|
205
219
|
| PropertyReference<TContext>
|
|
206
220
|
| Array<PropertyReference<TContext>>
|
|
207
221
|
|
|
208
|
-
export type Having<TContext extends Context = Context> = Condition<TContext>
|
|
209
|
-
|
|
210
222
|
export type Limit<TContext extends Context = Context> = number
|
|
211
223
|
|
|
212
224
|
export type Offset<TContext extends Context = Context> = number
|
|
@@ -220,9 +232,9 @@ export interface BaseQuery<TContext extends Context = Context> {
|
|
|
220
232
|
as?: As<TContext>
|
|
221
233
|
from: From<TContext>
|
|
222
234
|
join?: Array<JoinClause<TContext>>
|
|
223
|
-
where?:
|
|
235
|
+
where?: Where<TContext>
|
|
224
236
|
groupBy?: GroupBy<TContext>
|
|
225
|
-
having?:
|
|
237
|
+
having?: Having<TContext>
|
|
226
238
|
orderBy?: OrderBy<TContext>
|
|
227
239
|
limit?: Limit<TContext>
|
|
228
240
|
offset?: Offset<TContext>
|
package/src/query/select.ts
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
evaluateOperandOnNamespacedRow,
|
|
4
4
|
extractValueFromNamespacedRow,
|
|
5
5
|
} from "./extractors"
|
|
6
|
-
import type { ConditionOperand, Query } from "./schema"
|
|
6
|
+
import type { ConditionOperand, Query, SelectCallback } from "./schema"
|
|
7
7
|
import type { KeyedStream, NamespacedAndKeyedStream } from "../types"
|
|
8
8
|
|
|
9
9
|
export function processSelect(
|
|
@@ -31,6 +31,29 @@ export function processSelect(
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
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
|
+
|
|
34
57
|
if (typeof item === `string`) {
|
|
35
58
|
// Handle wildcard select - all columns from all tables
|
|
36
59
|
if ((item as string) === `@*`) {
|
package/src/transactions.ts
CHANGED
|
@@ -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() {
|
|
@@ -181,11 +182,17 @@ export class Transaction {
|
|
|
181
182
|
|
|
182
183
|
if (this.mutations.length === 0) {
|
|
183
184
|
this.setState(`completed`)
|
|
185
|
+
|
|
186
|
+
return this
|
|
184
187
|
}
|
|
185
188
|
|
|
186
189
|
// Run mutationFn
|
|
187
190
|
try {
|
|
188
|
-
|
|
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 })
|
|
189
196
|
|
|
190
197
|
this.setState(`completed`)
|
|
191
198
|
this.touchCollection()
|
package/src/types.ts
CHANGED
|
@@ -32,6 +32,16 @@ export type MutationFnParams = {
|
|
|
32
32
|
|
|
33
33
|
export type MutationFn = (params: MutationFnParams) => Promise<any>
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Utility type for a Transaction with at least one mutation
|
|
37
|
+
* This is used internally by the Transaction.commit method
|
|
38
|
+
*/
|
|
39
|
+
export type TransactionWithMutations<
|
|
40
|
+
T extends object = Record<string, unknown>,
|
|
41
|
+
> = Transaction & {
|
|
42
|
+
mutations: [PendingMutation<T>, ...Array<PendingMutation<T>>]
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
export interface TransactionConfig {
|
|
36
46
|
/** Unique identifier for the transaction */
|
|
37
47
|
id?: string
|
|
@@ -130,6 +140,24 @@ export interface CollectionConfig<T extends object = Record<string, unknown>> {
|
|
|
130
140
|
* getId: (item) => item.uuid
|
|
131
141
|
*/
|
|
132
142
|
getId: (item: T) => any
|
|
143
|
+
/**
|
|
144
|
+
* Optional asynchronous handler function called before an insert operation
|
|
145
|
+
* @param params Object containing transaction and mutation information
|
|
146
|
+
* @returns Promise resolving to any value
|
|
147
|
+
*/
|
|
148
|
+
onInsert?: MutationFn
|
|
149
|
+
/**
|
|
150
|
+
* Optional asynchronous handler function called before an update operation
|
|
151
|
+
* @param params Object containing transaction and mutation information
|
|
152
|
+
* @returns Promise resolving to any value
|
|
153
|
+
*/
|
|
154
|
+
onUpdate?: MutationFn
|
|
155
|
+
/**
|
|
156
|
+
* Optional asynchronous handler function called before a delete operation
|
|
157
|
+
* @param params Object containing transaction and mutation information
|
|
158
|
+
* @returns Promise resolving to any value
|
|
159
|
+
*/
|
|
160
|
+
onDelete?: MutationFn
|
|
133
161
|
}
|
|
134
162
|
|
|
135
163
|
export type ChangesPayload<T extends object = Record<string, unknown>> = Array<
|