@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
@@ -1,5 +1,5 @@
1
1
  import { Collection } from '../collection.cjs';
2
- import { Comparator, Condition, Limit, LiteralValue, Offset, OrderBy, Query, Select } from './schema.js';
2
+ import { Comparator, Condition, Limit, LiteralValue, Offset, OrderBy, Query, Select, WhereCallback } from './schema.js';
3
3
  import { Context, Flatten, InferResultTypeFromSelectTuple, Input, InputReference, PropertyReference, PropertyReferenceString, RemoveIndexSignature, Schema } from './types.js';
4
4
  type CollectionRef = {
5
5
  [K: string]: Collection<any>;
@@ -44,8 +44,9 @@ export declare class BaseQueryBuilder<TContext extends Context<Schema>> {
44
44
  /**
45
45
  * Specify what columns to select.
46
46
  * Overwrites any previous select clause.
47
+ * Also supports callback functions that receive the row context and return selected data.
47
48
  *
48
- * @param selects The columns to select
49
+ * @param selects The columns to select (can include callbacks)
49
50
  * @returns A new QueryBuilder with the select clause set
50
51
  */
51
52
  select<TSelects extends Array<Select<TContext>>>(this: QueryBuilder<TContext>, ...selects: TSelects): QueryBuilder<Flatten<Omit<TContext, `result`> & {
@@ -59,6 +60,10 @@ export declare class BaseQueryBuilder<TContext extends Context<Schema>> {
59
60
  * Add a where clause with a complete condition object.
60
61
  */
61
62
  where(condition: Condition<TContext>): QueryBuilder<TContext>;
63
+ /**
64
+ * Add a where clause with a callback function.
65
+ */
66
+ where(callback: WhereCallback<TContext>): QueryBuilder<TContext>;
62
67
  /**
63
68
  * Add a having clause comparing two values.
64
69
  * For filtering results after they have been grouped.
@@ -69,6 +74,11 @@ export declare class BaseQueryBuilder<TContext extends Context<Schema>> {
69
74
  * For filtering results after they have been grouped.
70
75
  */
71
76
  having(condition: Condition<TContext>): QueryBuilder<TContext>;
77
+ /**
78
+ * Add a having clause with a callback function.
79
+ * For filtering results after they have been grouped.
80
+ */
81
+ having(callback: WhereCallback<TContext>): QueryBuilder<TContext>;
72
82
  /**
73
83
  * Add a join clause to the query using a CollectionRef.
74
84
  */
@@ -91,6 +101,7 @@ export declare class BaseQueryBuilder<TContext extends Context<Schema>> {
91
101
  schema: TContext[`schema`] & {
92
102
  [K in keyof TCollectionRef & string]: RemoveIndexSignature<(TCollectionRef[keyof TCollectionRef] extends Collection<infer T> ? T : never) & Input>;
93
103
  };
104
+ hasJoin: true;
94
105
  }>>;
95
106
  /**
96
107
  * Add a join clause to the query without specifying an alias.
@@ -118,6 +129,7 @@ export declare class BaseQueryBuilder<TContext extends Context<Schema>> {
118
129
  schema: TContext[`schema`] & {
119
130
  [K in T]: RemoveIndexSignature<TContext[`baseSchema`][T]>;
120
131
  };
132
+ hasJoin: true;
121
133
  }>>;
122
134
  /**
123
135
  * Add a join clause to the query with a specified alias.
@@ -145,6 +157,7 @@ export declare class BaseQueryBuilder<TContext extends Context<Schema>> {
145
157
  schema: TContext[`schema`] & {
146
158
  [K in TAs]: RemoveIndexSignature<TContext[`baseSchema`][TFrom]>;
147
159
  };
160
+ hasJoin: true;
148
161
  }>>;
149
162
  private joinCollectionRef;
150
163
  private joinInputReference;
@@ -170,13 +183,6 @@ export declare class BaseQueryBuilder<TContext extends Context<Schema>> {
170
183
  * @returns A new QueryBuilder with the offset set
171
184
  */
172
185
  offset(offset: Offset<TContext>): QueryBuilder<TContext>;
173
- /**
174
- * Specify which column(s) to use as keys in the output keyed stream.
175
- *
176
- * @param keyBy The column(s) to use as keys
177
- * @returns A new QueryBuilder with the keyBy clause set
178
- */
179
- keyBy(keyBy: PropertyReference<TContext> | Array<PropertyReference<TContext>>): QueryBuilder<TContext>;
180
186
  /**
181
187
  * Add a groupBy clause to group the results by one or more columns.
182
188
  *
@@ -212,7 +218,7 @@ export declare function queryBuilder<TBaseSchema extends Schema = {}>(): Initial
212
218
  baseSchema: TBaseSchema;
213
219
  schema: {};
214
220
  }>;
215
- export type ResultsFromContext<TContext extends Context<Schema>> = Flatten<TContext[`result`] extends object ? TContext[`result`] : TContext[`result`] extends undefined ? TContext[`schema`] : object>;
221
+ export type ResultsFromContext<TContext extends Context<Schema>> = Flatten<TContext[`result`] extends object ? TContext[`result`] : TContext[`hasJoin`] extends true ? TContext[`schema`] : TContext[`default`] extends keyof TContext[`schema`] ? TContext[`schema`][TContext[`default`]] : never>;
216
222
  export type ResultFromQueryBuilder<TQueryBuilder> = Flatten<TQueryBuilder extends QueryBuilder<infer C> ? C extends {
217
223
  result: infer R;
218
224
  } ? R : never : never>;
@@ -51,7 +51,6 @@ export interface JoinClause<TContext extends Context = Context> {
51
51
  from: string;
52
52
  as?: string;
53
53
  on: Condition<TContext>;
54
- where?: Condition<TContext>;
55
54
  }
56
55
  export type OrderBy<TContext extends Context = Context> = PropertyReferenceString<TContext> | {
57
56
  [column in PropertyReferenceString<TContext>]?: `asc` | `desc`;
@@ -60,31 +59,36 @@ export type OrderBy<TContext extends Context = Context> = PropertyReferenceStrin
60
59
  }>;
61
60
  export type Select<TContext extends Context = Context> = PropertyReferenceString<TContext> | {
62
61
  [alias: string]: PropertyReference<TContext> | FunctionCall<TContext> | AggregateFunctionCall<TContext>;
63
- } | WildcardReferenceString<TContext>;
62
+ } | WildcardReferenceString<TContext> | SelectCallback<TContext>;
63
+ export type SelectCallback<TContext extends Context = Context> = (context: TContext extends {
64
+ schema: infer S;
65
+ } ? S : any) => any;
64
66
  export type As<TContext extends Context = Context> = string;
65
67
  export type From<TContext extends Context = Context> = InputReference<{
66
68
  baseSchema: TContext[`baseSchema`];
67
69
  schema: TContext[`baseSchema`];
68
70
  }>;
69
- export type Where<TContext extends Context = Context> = Condition<TContext>;
71
+ export type WhereCallback<TContext extends Context = Context> = (context: TContext extends {
72
+ schema: infer S;
73
+ } ? S : any) => boolean;
74
+ export type Where<TContext extends Context = Context> = Array<Condition<TContext> | WhereCallback<TContext>>;
75
+ export type Having<TContext extends Context = Context> = Where<TContext>;
70
76
  export type GroupBy<TContext extends Context = Context> = PropertyReference<TContext> | Array<PropertyReference<TContext>>;
71
- export type Having<TContext extends Context = Context> = Condition<TContext>;
72
77
  export type Limit<TContext extends Context = Context> = number;
73
78
  export type Offset<TContext extends Context = Context> = number;
74
79
  export interface BaseQuery<TContext extends Context = Context> {
75
- select: Array<Select<TContext>>;
80
+ select?: Array<Select<TContext>>;
76
81
  as?: As<TContext>;
77
82
  from: From<TContext>;
78
83
  join?: Array<JoinClause<TContext>>;
79
- where?: Condition<TContext>;
84
+ where?: Where<TContext>;
80
85
  groupBy?: GroupBy<TContext>;
81
- having?: Condition<TContext>;
86
+ having?: Having<TContext>;
82
87
  orderBy?: OrderBy<TContext>;
83
88
  limit?: Limit<TContext>;
84
89
  offset?: Offset<TContext>;
85
90
  }
86
91
  export interface Query<TContext extends Context = Context> extends BaseQuery<TContext> {
87
- keyBy?: PropertyReference<TContext> | Array<PropertyReference<TContext>>;
88
92
  with?: Array<WithQuery<TContext>>;
89
93
  collections?: {
90
94
  [K: string]: Collection<any>;
@@ -93,6 +97,3 @@ export interface Query<TContext extends Context = Context> extends BaseQuery<TCo
93
97
  export interface WithQuery<TContext extends Context = Context> extends BaseQuery<TContext> {
94
98
  as: string;
95
99
  }
96
- export interface KeyedQuery<TContext extends Context = Context> extends Query<TContext> {
97
- keyBy: PropertyReference<TContext> | Array<PropertyReference<TContext>>;
98
- }
@@ -4,18 +4,36 @@ const d2ts = require("@electric-sql/d2ts");
4
4
  const extractors = require("./extractors.cjs");
5
5
  function processSelect(pipeline, query, mainTableAlias, inputs) {
6
6
  return pipeline.pipe(
7
- d2ts.map((nestedRow) => {
7
+ d2ts.map(([key, namespacedRow]) => {
8
8
  const result = {};
9
- const isGroupedResult = query.groupBy && Object.keys(nestedRow).some(
10
- (key) => !Object.keys(inputs).includes(key) && typeof nestedRow[key] !== `object`
9
+ const isGroupedResult = query.groupBy && Object.keys(namespacedRow).some(
10
+ (namespaceKey) => !Object.keys(inputs).includes(namespaceKey) && typeof namespacedRow[namespaceKey] !== `object`
11
11
  );
12
+ if (!query.select) {
13
+ throw new Error(`Cannot process missing SELECT clause`);
14
+ }
12
15
  for (const item of query.select) {
16
+ if (typeof item === `function`) {
17
+ const callback = item;
18
+ const callbackResult = callback(namespacedRow);
19
+ if (callbackResult && typeof callbackResult === `object` && !Array.isArray(callbackResult)) {
20
+ Object.assign(result, callbackResult);
21
+ } else {
22
+ console.warn(
23
+ `SelectCallback returned a non-object value. SelectCallbacks should return objects with key-value pairs.`
24
+ );
25
+ }
26
+ continue;
27
+ }
13
28
  if (typeof item === `string`) {
14
29
  if (item === `@*`) {
15
30
  if (isGroupedResult) {
16
- Object.assign(result, nestedRow);
31
+ Object.assign(result, namespacedRow);
17
32
  } else {
18
- Object.assign(result, extractAllColumnsFromAllTables(nestedRow));
33
+ Object.assign(
34
+ result,
35
+ extractAllColumnsFromAllTables(namespacedRow)
36
+ );
19
37
  }
20
38
  continue;
21
39
  }
@@ -26,7 +44,7 @@ function processSelect(pipeline, query, mainTableAlias, inputs) {
26
44
  } else {
27
45
  Object.assign(
28
46
  result,
29
- extractAllColumnsFromTable(nestedRow, tableAlias)
47
+ extractAllColumnsFromTable(namespacedRow, tableAlias)
30
48
  );
31
49
  }
32
50
  continue;
@@ -34,11 +52,11 @@ function processSelect(pipeline, query, mainTableAlias, inputs) {
34
52
  if (item.startsWith(`@`)) {
35
53
  const columnRef = item.substring(1);
36
54
  const alias = columnRef;
37
- if (isGroupedResult && columnRef in nestedRow) {
38
- result[alias] = nestedRow[columnRef];
55
+ if (isGroupedResult && columnRef in namespacedRow) {
56
+ result[alias] = namespacedRow[columnRef];
39
57
  } else {
40
- result[alias] = extractors.extractValueFromNestedRow(
41
- nestedRow,
58
+ result[alias] = extractors.extractValueFromNamespacedRow(
59
+ namespacedRow,
42
60
  columnRef,
43
61
  mainTableAlias,
44
62
  void 0
@@ -54,22 +72,24 @@ function processSelect(pipeline, query, mainTableAlias, inputs) {
54
72
  for (const [alias, expr] of Object.entries(item)) {
55
73
  if (typeof expr === `string` && expr.startsWith(`@`)) {
56
74
  const columnRef = expr.substring(1);
57
- if (isGroupedResult && columnRef in nestedRow) {
58
- result[alias] = nestedRow[columnRef];
75
+ if (isGroupedResult && columnRef in namespacedRow) {
76
+ result[alias] = namespacedRow[columnRef];
59
77
  } else {
60
- result[alias] = extractors.extractValueFromNestedRow(
61
- nestedRow,
78
+ result[alias] = extractors.extractValueFromNamespacedRow(
79
+ namespacedRow,
62
80
  columnRef,
63
81
  mainTableAlias,
64
82
  void 0
65
83
  );
66
84
  }
67
85
  } else if (typeof expr === `object`) {
68
- if (isGroupedResult && alias in nestedRow) {
69
- result[alias] = nestedRow[alias];
86
+ if (isGroupedResult && alias in namespacedRow) {
87
+ result[alias] = namespacedRow[alias];
88
+ } else if (expr.ORDER_INDEX) {
89
+ result[alias] = namespacedRow[mainTableAlias][alias];
70
90
  } else {
71
- result[alias] = extractors.evaluateOperandOnNestedRow(
72
- nestedRow,
91
+ result[alias] = extractors.evaluateOperandOnNamespacedRow(
92
+ namespacedRow,
73
93
  expr,
74
94
  mainTableAlias,
75
95
  void 0
@@ -79,22 +99,25 @@ function processSelect(pipeline, query, mainTableAlias, inputs) {
79
99
  }
80
100
  }
81
101
  }
82
- return result;
102
+ return [key, result];
83
103
  })
84
104
  );
85
105
  }
86
- function extractAllColumnsFromAllTables(nestedRow) {
106
+ function extractAllColumnsFromAllTables(namespacedRow) {
87
107
  const result = {};
88
- for (const [tableAlias, tableData] of Object.entries(nestedRow)) {
108
+ for (const [tableAlias, tableData] of Object.entries(namespacedRow)) {
89
109
  if (tableData && typeof tableData === `object`) {
90
- Object.assign(result, extractAllColumnsFromTable(nestedRow, tableAlias));
110
+ Object.assign(
111
+ result,
112
+ extractAllColumnsFromTable(namespacedRow, tableAlias)
113
+ );
91
114
  }
92
115
  }
93
116
  return result;
94
117
  }
95
- function extractAllColumnsFromTable(nestedRow, tableAlias) {
118
+ function extractAllColumnsFromTable(namespacedRow, tableAlias) {
96
119
  const result = {};
97
- const tableData = nestedRow[tableAlias];
120
+ const tableData = namespacedRow[tableAlias];
98
121
  if (!tableData || typeof tableData !== `object`) {
99
122
  return result;
100
123
  }
@@ -1 +1 @@
1
- {"version":3,"file":"select.cjs","sources":["../../../src/query/select.ts"],"sourcesContent":["import { map } from \"@electric-sql/d2ts\"\nimport {\n evaluateOperandOnNestedRow,\n extractValueFromNestedRow,\n} from \"./extractors\"\nimport type { IStreamBuilder } from \"@electric-sql/d2ts\"\nimport type { ConditionOperand, Query } from \"./schema\"\n\nexport function processSelect(\n pipeline: IStreamBuilder<Record<string, unknown>>,\n query: Query,\n mainTableAlias: string,\n inputs: Record<string, IStreamBuilder<Record<string, unknown>>>\n) {\n return pipeline.pipe(\n map((nestedRow: Record<string, unknown>) => {\n const result: Record<string, unknown> = {}\n\n // Check if this is a grouped result (has no nested table structure)\n // If it's a grouped result, we need to handle it differently\n const isGroupedResult =\n query.groupBy &&\n Object.keys(nestedRow).some(\n (key) =>\n !Object.keys(inputs).includes(key) &&\n typeof nestedRow[key] !== `object`\n )\n\n for (const item of query.select) {\n if (typeof item === `string`) {\n // Handle wildcard select - all columns from all tables\n if ((item as string) === `@*`) {\n // For grouped results, just return the row as is\n if (isGroupedResult) {\n Object.assign(result, nestedRow)\n } else {\n // Extract all columns from all tables\n Object.assign(result, extractAllColumnsFromAllTables(nestedRow))\n }\n continue\n }\n\n // Handle @table.* syntax - all columns from a specific table\n if (\n (item as string).startsWith(`@`) &&\n (item as string).endsWith(`.*`)\n ) {\n const tableAlias = (item as string).slice(1, -2) // Remove the '@' and '.*' parts\n\n // For grouped results, check if we have columns from this table\n if (isGroupedResult) {\n // In grouped results, we don't have the nested structure anymore\n // So we can't extract by table. Just continue to the next item.\n continue\n } else {\n // Extract all columns from the specified table\n Object.assign(\n result,\n extractAllColumnsFromTable(nestedRow, tableAlias)\n )\n }\n continue\n }\n\n // Handle simple column references like \"@table.column\" or \"@column\"\n if ((item as string).startsWith(`@`)) {\n const columnRef = (item as string).substring(1)\n const alias = columnRef\n\n // For grouped results, check if the column is directly in the row first\n if (isGroupedResult && columnRef in nestedRow) {\n result[alias] = nestedRow[columnRef]\n } else {\n // Extract the value from the nested structure\n result[alias] = extractValueFromNestedRow(\n nestedRow,\n columnRef,\n mainTableAlias,\n undefined\n )\n }\n\n // If the alias contains a dot (table.column),\n // use just the column part as the field name\n if (alias.includes(`.`)) {\n const columnName = alias.split(`.`)[1]\n result[columnName!] = result[alias]\n delete result[alias]\n }\n }\n } else {\n // Handle aliased columns like { alias: \"@column_name\" }\n for (const [alias, expr] of Object.entries(item)) {\n if (typeof expr === `string` && (expr as string).startsWith(`@`)) {\n const columnRef = (expr as string).substring(1)\n\n // For grouped results, check if the column is directly in the row first\n if (isGroupedResult && columnRef in nestedRow) {\n result[alias] = nestedRow[columnRef]\n } else {\n // Extract the value from the nested structure\n result[alias] = extractValueFromNestedRow(\n nestedRow,\n columnRef,\n mainTableAlias,\n undefined\n )\n }\n } else if (typeof expr === `object`) {\n // For grouped results, the aggregate results are already in the row\n if (isGroupedResult && alias in nestedRow) {\n result[alias] = nestedRow[alias]\n } else {\n // This might be a function call\n result[alias] = evaluateOperandOnNestedRow(\n nestedRow,\n expr as ConditionOperand,\n mainTableAlias,\n undefined\n )\n }\n }\n }\n }\n }\n\n return result\n })\n )\n}\n\n// Helper function to extract all columns from all tables in a nested row\nfunction extractAllColumnsFromAllTables(\n nestedRow: Record<string, unknown>\n): Record<string, unknown> {\n const result: Record<string, unknown> = {}\n\n // Process each table in the nested row\n for (const [tableAlias, tableData] of Object.entries(nestedRow)) {\n if (tableData && typeof tableData === `object`) {\n // Add all columns from this table to the result\n // If there are column name conflicts, the last table's columns will overwrite previous ones\n Object.assign(result, extractAllColumnsFromTable(nestedRow, tableAlias))\n }\n }\n\n return result\n}\n\n// Helper function to extract all columns from a table in a nested row\nfunction extractAllColumnsFromTable(\n nestedRow: Record<string, unknown>,\n tableAlias: string\n): Record<string, unknown> {\n const result: Record<string, unknown> = {}\n\n // Get the table data\n const tableData = nestedRow[tableAlias] as\n | Record<string, unknown>\n | null\n | undefined\n\n if (!tableData || typeof tableData !== `object`) {\n return result\n }\n\n // Add all columns from the table to the result\n for (const [columnName, value] of Object.entries(tableData)) {\n result[columnName] = value\n }\n\n return result\n}\n"],"names":["map","extractValueFromNestedRow","evaluateOperandOnNestedRow"],"mappings":"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"}
1
+ {"version":3,"file":"select.cjs","sources":["../../../src/query/select.ts"],"sourcesContent":["import { map } from \"@electric-sql/d2ts\"\nimport {\n evaluateOperandOnNamespacedRow,\n extractValueFromNamespacedRow,\n} from \"./extractors\"\nimport type { ConditionOperand, Query, SelectCallback } from \"./schema\"\nimport type { KeyedStream, NamespacedAndKeyedStream } from \"../types\"\n\nexport function processSelect(\n pipeline: NamespacedAndKeyedStream,\n query: Query,\n mainTableAlias: string,\n inputs: Record<string, KeyedStream>\n): KeyedStream {\n return pipeline.pipe(\n map(([key, namespacedRow]) => {\n const result: Record<string, unknown> = {}\n\n // Check if this is a grouped result (has no nested table structure)\n // If it's a grouped result, we need to handle it differently\n const isGroupedResult =\n query.groupBy &&\n Object.keys(namespacedRow).some(\n (namespaceKey) =>\n !Object.keys(inputs).includes(namespaceKey) &&\n typeof namespacedRow[namespaceKey] !== `object`\n )\n\n if (!query.select) {\n throw new Error(`Cannot process missing SELECT clause`)\n }\n\n for (const item of query.select) {\n // Handle callback functions\n if (typeof item === `function`) {\n const callback = item as SelectCallback\n const callbackResult = callback(namespacedRow)\n\n // If the callback returns an object, merge its properties into the result\n if (\n callbackResult &&\n typeof callbackResult === `object` &&\n !Array.isArray(callbackResult)\n ) {\n Object.assign(result, callbackResult)\n } else {\n // If the callback returns a primitive value, we can't merge it\n // This would need a specific key, but since we don't have one, we'll skip it\n // In practice, select callbacks should return objects with keys\n console.warn(\n `SelectCallback returned a non-object value. SelectCallbacks should return objects with key-value pairs.`\n )\n }\n continue\n }\n\n if (typeof item === `string`) {\n // Handle wildcard select - all columns from all tables\n if ((item as string) === `@*`) {\n // For grouped results, just return the row as is\n if (isGroupedResult) {\n Object.assign(result, namespacedRow)\n } else {\n // Extract all columns from all tables\n Object.assign(\n result,\n extractAllColumnsFromAllTables(namespacedRow)\n )\n }\n continue\n }\n\n // Handle @table.* syntax - all columns from a specific table\n if (\n (item as string).startsWith(`@`) &&\n (item as string).endsWith(`.*`)\n ) {\n const tableAlias = (item as string).slice(1, -2) // Remove the '@' and '.*' parts\n\n // For grouped results, check if we have columns from this table\n if (isGroupedResult) {\n // In grouped results, we don't have the nested structure anymore\n // So we can't extract by table. Just continue to the next item.\n continue\n } else {\n // Extract all columns from the specified table\n Object.assign(\n result,\n extractAllColumnsFromTable(namespacedRow, tableAlias)\n )\n }\n continue\n }\n\n // Handle simple column references like \"@table.column\" or \"@column\"\n if ((item as string).startsWith(`@`)) {\n const columnRef = (item as string).substring(1)\n const alias = columnRef\n\n // For grouped results, check if the column is directly in the row first\n if (isGroupedResult && columnRef in namespacedRow) {\n result[alias] = namespacedRow[columnRef]\n } else {\n // Extract the value from the nested structure\n result[alias] = extractValueFromNamespacedRow(\n namespacedRow,\n columnRef,\n mainTableAlias,\n undefined\n )\n }\n\n // If the alias contains a dot (table.column),\n // use just the column part as the field name\n if (alias.includes(`.`)) {\n const columnName = alias.split(`.`)[1]\n result[columnName!] = result[alias]\n delete result[alias]\n }\n }\n } else {\n // Handle aliased columns like { alias: \"@column_name\" }\n for (const [alias, expr] of Object.entries(item)) {\n if (typeof expr === `string` && (expr as string).startsWith(`@`)) {\n const columnRef = (expr as string).substring(1)\n\n // For grouped results, check if the column is directly in the row first\n if (isGroupedResult && columnRef in namespacedRow) {\n result[alias] = namespacedRow[columnRef]\n } else {\n // Extract the value from the nested structure\n result[alias] = extractValueFromNamespacedRow(\n namespacedRow,\n columnRef,\n mainTableAlias,\n undefined\n )\n }\n } else if (typeof expr === `object`) {\n // For grouped results, the aggregate results are already in the row\n if (isGroupedResult && alias in namespacedRow) {\n result[alias] = namespacedRow[alias]\n } else if ((expr as { ORDER_INDEX: unknown }).ORDER_INDEX) {\n result[alias] = namespacedRow[mainTableAlias]![alias]\n } else {\n // This might be a function call\n result[alias] = evaluateOperandOnNamespacedRow(\n namespacedRow,\n expr as ConditionOperand,\n mainTableAlias,\n undefined\n )\n }\n }\n }\n }\n }\n\n return [key, result] as [string, typeof result]\n })\n )\n}\n\n// Helper function to extract all columns from all tables in a nested row\nfunction extractAllColumnsFromAllTables(\n namespacedRow: Record<string, unknown>\n): Record<string, unknown> {\n const result: Record<string, unknown> = {}\n\n // Process each table in the nested row\n for (const [tableAlias, tableData] of Object.entries(namespacedRow)) {\n if (tableData && typeof tableData === `object`) {\n // Add all columns from this table to the result\n // If there are column name conflicts, the last table's columns will overwrite previous ones\n Object.assign(\n result,\n extractAllColumnsFromTable(namespacedRow, tableAlias)\n )\n }\n }\n\n return result\n}\n\n// Helper function to extract all columns from a table in a nested row\nfunction extractAllColumnsFromTable(\n namespacedRow: Record<string, unknown>,\n tableAlias: string\n): Record<string, unknown> {\n const result: Record<string, unknown> = {}\n\n // Get the table data\n const tableData = namespacedRow[tableAlias] as\n | Record<string, unknown>\n | null\n | undefined\n\n if (!tableData || typeof tableData !== `object`) {\n return result\n }\n\n // Add all columns from the table to the result\n for (const [columnName, value] of Object.entries(tableData)) {\n result[columnName] = value\n }\n\n return result\n}\n"],"names":["map","extractValueFromNamespacedRow","evaluateOperandOnNamespacedRow"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
- import { IStreamBuilder } from '@electric-sql/d2ts';
2
1
  import { Query } from './schema.cjs';
3
- export declare function processSelect(pipeline: IStreamBuilder<Record<string, unknown>>, query: Query, mainTableAlias: string, inputs: Record<string, IStreamBuilder<Record<string, unknown>>>): IStreamBuilder<Record<string, unknown>>;
2
+ import { KeyedStream, NamespacedAndKeyedStream } from '../types.cjs';
3
+ export declare function processSelect(pipeline: NamespacedAndKeyedStream, query: Query, mainTableAlias: string, inputs: Record<string, KeyedStream>): KeyedStream;
@@ -6,6 +6,7 @@ export type Context<TBaseSchema extends Schema = Schema, TSchema extends Schema
6
6
  schema: TSchema;
7
7
  default?: keyof TSchema;
8
8
  result?: Record<string, unknown>;
9
+ hasJoin?: boolean;
9
10
  };
10
11
  export type Flatten<T> = {
11
12
  [K in keyof T]: T[K];
@@ -12,6 +12,7 @@ function generateUUID() {
12
12
  });
13
13
  }
14
14
  const transactions = [];
15
+ let transactionStack = [];
15
16
  function createTransaction(config) {
16
17
  if (typeof config.mutationFn === `undefined`) {
17
18
  throw `mutationFn is required when creating a transaction`;
@@ -24,7 +25,6 @@ function createTransaction(config) {
24
25
  transactions.push(newTransaction);
25
26
  return newTransaction;
26
27
  }
27
- let transactionStack = [];
28
28
  function getActiveTransaction() {
29
29
  if (transactionStack.length > 0) {
30
30
  return transactionStack.slice(-1)[0];
@@ -38,6 +38,12 @@ function registerTransaction(tx) {
38
38
  function unregisterTransaction(tx) {
39
39
  transactionStack = transactionStack.filter((t) => t.id !== tx.id);
40
40
  }
41
+ function removeFromPendingList(tx) {
42
+ const index = transactions.findIndex((t) => t.id === tx.id);
43
+ if (index !== -1) {
44
+ transactions.splice(index, 1);
45
+ }
46
+ }
41
47
  class Transaction {
42
48
  constructor(config) {
43
49
  this.id = config.id;
@@ -51,6 +57,9 @@ class Transaction {
51
57
  }
52
58
  setState(newState) {
53
59
  this.state = newState;
60
+ if (newState === `completed` || newState === `failed`) {
61
+ removeFromPendingList(this);
62
+ }
54
63
  }
55
64
  mutate(callback) {
56
65
  if (this.state !== `pending`) {
@@ -87,11 +96,11 @@ class Transaction {
87
96
  }
88
97
  this.setState(`failed`);
89
98
  if (!isSecondaryRollback) {
90
- const mutationKeys = /* @__PURE__ */ new Set();
91
- this.mutations.forEach((m) => mutationKeys.add(m.key));
92
- transactions.forEach(
93
- (t) => t.state === `pending` && t.mutations.some((m) => mutationKeys.has(m.key)) && t.rollback({ isSecondaryRollback: true })
94
- );
99
+ const mutationIds = /* @__PURE__ */ new Set();
100
+ this.mutations.forEach((m) => mutationIds.add(m.key));
101
+ for (const t of transactions) {
102
+ t.state === `pending` && t.mutations.some((m) => mutationIds.has(m.key)) && t.rollback({ isSecondaryRollback: true });
103
+ }
95
104
  }
96
105
  this.isPersisted.reject((_a = this.error) == null ? void 0 : _a.error);
97
106
  this.touchCollection();
@@ -100,13 +109,13 @@ class Transaction {
100
109
  // Tell collection that something has changed with the transaction
101
110
  touchCollection() {
102
111
  const hasCalled = /* @__PURE__ */ new Set();
103
- this.mutations.forEach((mutation) => {
112
+ for (const mutation of this.mutations) {
104
113
  if (!hasCalled.has(mutation.collection.id)) {
105
114
  mutation.collection.transactions.setState((state) => state);
106
115
  mutation.collection.commitPendingTransactions();
107
116
  hasCalled.add(mutation.collection.id);
108
117
  }
109
- });
118
+ }
110
119
  }
111
120
  async commit() {
112
121
  if (this.state !== `pending`) {
@@ -115,9 +124,11 @@ class Transaction {
115
124
  this.setState(`persisting`);
116
125
  if (this.mutations.length === 0) {
117
126
  this.setState(`completed`);
127
+ return this;
118
128
  }
119
129
  try {
120
- await this.mutationFn({ transaction: this });
130
+ const transactionWithMutations = this;
131
+ await this.mutationFn({ transaction: transactionWithMutations });
121
132
  this.setState(`completed`);
122
133
  this.touchCollection();
123
134
  this.isPersisted.resolve(this);
@@ -1 +1 @@
1
- {"version":3,"file":"transactions.cjs","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n PendingMutation,\n TransactionConfig,\n TransactionState,\n} from \"./types\"\n\nfunction generateUUID() {\n // Check if crypto.randomUUID is available (modern browsers and Node.js 15+)\n if (\n typeof crypto !== `undefined` &&\n typeof crypto.randomUUID === `function`\n ) {\n return crypto.randomUUID()\n }\n\n // Fallback implementation for older environments\n return `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`.replace(/[xy]/g, function (c) {\n const r = (Math.random() * 16) | 0\n const v = c === `x` ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n}\n\nconst transactions: Array<Transaction> = []\n\nexport function createTransaction(config: TransactionConfig): Transaction {\n if (typeof config.mutationFn === `undefined`) {\n throw `mutationFn is required when creating a transaction`\n }\n\n let transactionId = config.id\n if (!transactionId) {\n transactionId = generateUUID()\n }\n const newTransaction = new Transaction({ ...config, id: transactionId })\n transactions.push(newTransaction)\n\n return newTransaction\n}\n\nlet transactionStack: Array<Transaction> = []\n\nexport function getActiveTransaction(): Transaction | undefined {\n if (transactionStack.length > 0) {\n return transactionStack.slice(-1)[0]\n } else {\n return undefined\n }\n}\n\nfunction registerTransaction(tx: Transaction) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nexport class Transaction {\n public id: string\n public state: TransactionState\n public mutationFn\n public mutations: Array<PendingMutation<any>>\n public isPersisted: Deferred<Transaction>\n public autoCommit: boolean\n public createdAt: Date\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig) {\n this.id = config.id!\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.metadata = config.metadata ?? {}\n }\n\n setState(newState: TransactionState) {\n this.state = newState\n }\n\n mutate(callback: () => void): Transaction {\n if (this.state !== `pending`) {\n throw `You can no longer call .mutate() as the transaction is no longer pending`\n }\n\n registerTransaction(this)\n try {\n callback()\n } finally {\n unregisterTransaction(this)\n }\n\n if (this.autoCommit) {\n this.commit()\n }\n\n return this\n }\n\n applyMutations(mutations: Array<PendingMutation<any>>): void {\n for (const newMutation of mutations) {\n const existingIndex = this.mutations.findIndex(\n (m) => m.key === newMutation.key\n )\n\n if (existingIndex >= 0) {\n // Replace existing mutation\n this.mutations[existingIndex] = newMutation\n } else {\n // Insert new mutation\n this.mutations.push(newMutation)\n }\n }\n }\n\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction {\n const isSecondaryRollback = config?.isSecondaryRollback ?? false\n if (this.state === `completed`) {\n throw `You can no longer call .rollback() as the transaction is already completed`\n }\n\n this.setState(`failed`)\n\n // See if there's any other transactions w/ mutations on the same keys\n // and roll them back as well.\n if (!isSecondaryRollback) {\n const mutationKeys = new Set()\n this.mutations.forEach((m) => mutationKeys.add(m.key))\n transactions.forEach(\n (t) =>\n t.state === `pending` &&\n t.mutations.some((m) => mutationKeys.has(m.key)) &&\n t.rollback({ isSecondaryRollback: true })\n )\n }\n\n // Reject the promise\n this.isPersisted.reject(this.error?.error)\n\n this.touchCollection()\n\n return this\n }\n\n // Tell collection that something has changed with the transaction\n touchCollection(): void {\n const hasCalled = new Set()\n this.mutations.forEach((mutation) => {\n if (!hasCalled.has(mutation.collection.id)) {\n mutation.collection.transactions.setState((state) => state)\n mutation.collection.commitPendingTransactions()\n hasCalled.add(mutation.collection.id)\n }\n })\n }\n\n async commit(): Promise<Transaction> {\n if (this.state !== `pending`) {\n throw `You can no longer call .commit() as the transaction is no longer pending`\n }\n\n this.setState(`persisting`)\n\n if (this.mutations.length === 0) {\n this.setState(`completed`)\n }\n\n // Run mutationFn\n try {\n await this.mutationFn({ transaction: this })\n\n this.setState(`completed`)\n this.touchCollection()\n\n this.isPersisted.resolve(this)\n } catch (error) {\n // Update transaction with error information\n this.error = {\n message: error instanceof Error ? error.message : String(error),\n error: error instanceof Error ? error : new Error(String(error)),\n }\n\n // rollback the transaction\n return this.rollback()\n }\n\n return this\n }\n}\n"],"names":["createDeferred"],"mappings":"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"}
1
+ {"version":3,"file":"transactions.cjs","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n PendingMutation,\n TransactionConfig,\n TransactionState,\n TransactionWithMutations,\n} from \"./types\"\n\nfunction generateUUID() {\n // Check if crypto.randomUUID is available (modern browsers and Node.js 15+)\n if (\n typeof crypto !== `undefined` &&\n typeof crypto.randomUUID === `function`\n ) {\n return crypto.randomUUID()\n }\n\n // Fallback implementation for older environments\n return `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`.replace(/[xy]/g, function (c) {\n const r = (Math.random() * 16) | 0\n const v = c === `x` ? r : (r & 0x3) | 0x8\n return v.toString(16)\n })\n}\n\nconst transactions: Array<Transaction> = []\nlet transactionStack: Array<Transaction> = []\n\nexport function createTransaction(config: TransactionConfig): Transaction {\n if (typeof config.mutationFn === `undefined`) {\n throw `mutationFn is required when creating a transaction`\n }\n\n let transactionId = config.id\n if (!transactionId) {\n transactionId = generateUUID()\n }\n const newTransaction = new Transaction({ ...config, id: transactionId })\n\n transactions.push(newTransaction)\n\n return newTransaction\n}\n\nexport function getActiveTransaction(): Transaction | undefined {\n if (transactionStack.length > 0) {\n return transactionStack.slice(-1)[0]\n } else {\n return undefined\n }\n}\n\nfunction registerTransaction(tx: Transaction) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nexport class Transaction {\n public id: string\n public state: TransactionState\n public mutationFn\n public mutations: Array<PendingMutation<any>>\n public isPersisted: Deferred<Transaction>\n public autoCommit: boolean\n public createdAt: Date\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig) {\n this.id = config.id!\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.metadata = config.metadata ?? {}\n }\n\n setState(newState: TransactionState) {\n this.state = newState\n\n if (newState === `completed` || newState === `failed`) {\n removeFromPendingList(this)\n }\n }\n\n mutate(callback: () => void): Transaction {\n if (this.state !== `pending`) {\n throw `You can no longer call .mutate() as the transaction is no longer pending`\n }\n\n registerTransaction(this)\n try {\n callback()\n } finally {\n unregisterTransaction(this)\n }\n\n if (this.autoCommit) {\n this.commit()\n }\n\n return this\n }\n\n applyMutations(mutations: Array<PendingMutation<any>>): void {\n for (const newMutation of mutations) {\n const existingIndex = this.mutations.findIndex(\n (m) => m.key === newMutation.key\n )\n\n if (existingIndex >= 0) {\n // Replace existing mutation\n this.mutations[existingIndex] = newMutation\n } else {\n // Insert new mutation\n this.mutations.push(newMutation)\n }\n }\n }\n\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction {\n const isSecondaryRollback = config?.isSecondaryRollback ?? false\n if (this.state === `completed`) {\n throw `You can no longer call .rollback() as the transaction is already completed`\n }\n\n this.setState(`failed`)\n\n // See if there's any other transactions w/ mutations on the same ids\n // and roll them back as well.\n if (!isSecondaryRollback) {\n const mutationIds = new Set()\n this.mutations.forEach((m) => mutationIds.add(m.key))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.key)) &&\n t.rollback({ isSecondaryRollback: true })\n }\n }\n\n // Reject the promise\n this.isPersisted.reject(this.error?.error)\n this.touchCollection()\n\n return this\n }\n\n // Tell collection that something has changed with the transaction\n touchCollection(): void {\n const hasCalled = new Set()\n for (const mutation of this.mutations) {\n if (!hasCalled.has(mutation.collection.id)) {\n mutation.collection.transactions.setState((state) => state)\n mutation.collection.commitPendingTransactions()\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n async commit(): Promise<Transaction> {\n if (this.state !== `pending`) {\n throw `You can no longer call .commit() as the transaction is no longer pending`\n }\n\n this.setState(`persisting`)\n\n if (this.mutations.length === 0) {\n this.setState(`completed`)\n\n return this\n }\n\n // Run mutationFn\n try {\n // At this point we know there's at least one mutation\n // Use type assertion to tell TypeScript about this guarantee\n const transactionWithMutations =\n this as unknown as TransactionWithMutations\n await this.mutationFn({ transaction: transactionWithMutations })\n\n this.setState(`completed`)\n this.touchCollection()\n\n this.isPersisted.resolve(this)\n } catch (error) {\n // Update transaction with error information\n this.error = {\n message: error instanceof Error ? error.message : String(error),\n error: error instanceof Error ? error : new Error(String(error)),\n }\n\n // rollback the transaction\n return this.rollback()\n }\n\n return this\n }\n}\n"],"names":["createDeferred"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { IStreamBuilder } from '@electric-sql/d2ts';
1
2
  import { Collection } from './collection.cjs';
2
3
  import { StandardSchemaV1 } from '@standard-schema/spec';
3
4
  import { Transaction } from './transactions.cjs';
@@ -11,7 +12,7 @@ export interface PendingMutation<T extends object = Record<string, unknown>> {
11
12
  original: Record<string, unknown>;
12
13
  modified: Record<string, unknown>;
13
14
  changes: Record<string, unknown>;
14
- key: string;
15
+ key: any;
15
16
  type: OperationType;
16
17
  metadata: unknown;
17
18
  syncMetadata: Record<string, unknown>;
@@ -26,6 +27,13 @@ export type MutationFnParams = {
26
27
  transaction: Transaction;
27
28
  };
28
29
  export type MutationFn = (params: MutationFnParams) => Promise<any>;
30
+ /**
31
+ * Utility type for a Transaction with at least one mutation
32
+ * This is used internally by the Transaction.commit method
33
+ */
34
+ export type TransactionWithMutations<T extends object = Record<string, unknown>> = Transaction & {
35
+ mutations: [PendingMutation<T>, ...Array<PendingMutation<T>>];
36
+ };
29
37
  export interface TransactionConfig {
30
38
  /** Unique identifier for the transaction */
31
39
  id?: string;
@@ -36,7 +44,7 @@ export interface TransactionConfig {
36
44
  }
37
45
  export type { Transaction };
38
46
  type Value<TExtensions = never> = string | number | boolean | bigint | null | TExtensions | Array<Value<TExtensions>> | {
39
- [key: string]: Value<TExtensions>;
47
+ [key: string | number | symbol]: Value<TExtensions>;
40
48
  };
41
49
  export type Row<TExtensions = never> = Record<string, Value<TExtensions>>;
42
50
  export type OperationType = `insert` | `update` | `delete`;
@@ -44,17 +52,17 @@ export interface SyncConfig<T extends object = Record<string, unknown>> {
44
52
  sync: (params: {
45
53
  collection: Collection<T>;
46
54
  begin: () => void;
47
- write: (message: ChangeMessage<T>) => void;
55
+ write: (message: Omit<ChangeMessage<T>, `key`>) => void;
48
56
  commit: () => void;
49
57
  }) => void;
50
58
  /**
51
59
  * Get the sync metadata for insert operations
52
- * @returns Record containing primaryKey and relation information
60
+ * @returns Record containing relation information
53
61
  */
54
62
  getSyncMetadata?: () => Record<string, unknown>;
55
63
  }
56
64
  export interface ChangeMessage<T extends object = Record<string, unknown>> {
57
- key: string;
65
+ key: any;
58
66
  value: T;
59
67
  previousValue?: T;
60
68
  type: OperationType;
@@ -83,12 +91,63 @@ export interface OperationConfig {
83
91
  metadata?: Record<string, unknown>;
84
92
  }
85
93
  export interface InsertConfig {
86
- key?: string | Array<string | undefined>;
87
94
  metadata?: Record<string, unknown>;
88
95
  }
89
96
  export interface CollectionConfig<T extends object = Record<string, unknown>> {
90
- id: string;
97
+ id?: string;
91
98
  sync: SyncConfig<T>;
92
99
  schema?: StandardSchema<T>;
100
+ /**
101
+ * Function to extract the ID from an object
102
+ * This is required for update/delete operations which now only accept IDs
103
+ * @param item The item to extract the ID from
104
+ * @returns The ID string for the item
105
+ * @example
106
+ * // For a collection with a 'uuid' field as the primary key
107
+ * getId: (item) => item.uuid
108
+ */
109
+ getId: (item: T) => any;
110
+ /**
111
+ * Optional asynchronous handler function called before an insert operation
112
+ * @param params Object containing transaction and mutation information
113
+ * @returns Promise resolving to any value
114
+ */
115
+ onInsert?: MutationFn;
116
+ /**
117
+ * Optional asynchronous handler function called before an update operation
118
+ * @param params Object containing transaction and mutation information
119
+ * @returns Promise resolving to any value
120
+ */
121
+ onUpdate?: MutationFn;
122
+ /**
123
+ * Optional asynchronous handler function called before a delete operation
124
+ * @param params Object containing transaction and mutation information
125
+ * @returns Promise resolving to any value
126
+ */
127
+ onDelete?: MutationFn;
93
128
  }
94
129
  export type ChangesPayload<T extends object = Record<string, unknown>> = Array<ChangeMessage<T>>;
130
+ /**
131
+ * An input row from a collection
132
+ */
133
+ export type InputRow = [unknown, Record<string, unknown>];
134
+ /**
135
+ * A keyed stream is a stream of rows
136
+ * This is used as the inputs from a collection to a query
137
+ */
138
+ export type KeyedStream = IStreamBuilder<InputRow>;
139
+ /**
140
+ * A namespaced row is a row withing a pipeline that had each table wrapped in its alias
141
+ */
142
+ export type NamespacedRow = Record<string, Record<string, unknown>>;
143
+ /**
144
+ * A keyed namespaced row is a row with a key and a namespaced row
145
+ * This is the main representation of a row in a query pipeline
146
+ */
147
+ export type KeyedNamespacedRow = [unknown, NamespacedRow];
148
+ /**
149
+ * A namespaced and keyed stream is a stream of rows
150
+ * This is used throughout a query pipeline and as the output from a query without
151
+ * a `select` clause.
152
+ */
153
+ export type NamespacedAndKeyedStream = IStreamBuilder<KeyedNamespacedRow>;