@tanstack/db 0.0.21 → 0.0.23

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 (38) hide show
  1. package/dist/cjs/collection.cjs +14 -6
  2. package/dist/cjs/collection.cjs.map +1 -1
  3. package/dist/cjs/collection.d.cts +10 -9
  4. package/dist/cjs/local-storage.cjs +1 -1
  5. package/dist/cjs/local-storage.cjs.map +1 -1
  6. package/dist/cjs/query/builder/index.cjs +20 -0
  7. package/dist/cjs/query/builder/index.cjs.map +1 -1
  8. package/dist/cjs/query/builder/index.d.cts +15 -0
  9. package/dist/cjs/query/compiler/index.cjs +6 -0
  10. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  11. package/dist/cjs/query/ir.cjs.map +1 -1
  12. package/dist/cjs/query/ir.d.cts +1 -0
  13. package/dist/cjs/transactions.cjs.map +1 -1
  14. package/dist/cjs/transactions.d.cts +5 -5
  15. package/dist/cjs/types.d.cts +35 -10
  16. package/dist/esm/collection.d.ts +10 -9
  17. package/dist/esm/collection.js +14 -6
  18. package/dist/esm/collection.js.map +1 -1
  19. package/dist/esm/local-storage.js +1 -1
  20. package/dist/esm/local-storage.js.map +1 -1
  21. package/dist/esm/query/builder/index.d.ts +15 -0
  22. package/dist/esm/query/builder/index.js +20 -0
  23. package/dist/esm/query/builder/index.js.map +1 -1
  24. package/dist/esm/query/compiler/index.js +7 -1
  25. package/dist/esm/query/compiler/index.js.map +1 -1
  26. package/dist/esm/query/ir.d.ts +1 -0
  27. package/dist/esm/query/ir.js.map +1 -1
  28. package/dist/esm/transactions.d.ts +5 -5
  29. package/dist/esm/transactions.js.map +1 -1
  30. package/dist/esm/types.d.ts +35 -10
  31. package/package.json +2 -2
  32. package/src/collection.ts +62 -21
  33. package/src/local-storage.ts +2 -2
  34. package/src/query/builder/index.ts +21 -0
  35. package/src/query/compiler/index.ts +11 -2
  36. package/src/query/ir.ts +1 -0
  37. package/src/transactions.ts +8 -12
  38. package/src/types.ts +69 -14
@@ -369,6 +369,26 @@ class BaseQueryBuilder {
369
369
  offset: count
370
370
  });
371
371
  }
372
+ /**
373
+ * Specify that the query should return distinct rows.
374
+ * Deduplicates rows based on the selected columns.
375
+ * @returns A QueryBuilder with distinct enabled
376
+ *
377
+ * @example
378
+ * ```ts
379
+ * // Get countries our users are from
380
+ * query
381
+ * .from({ users: usersCollection })
382
+ * .select(({users}) => users.country)
383
+ * .distinct()
384
+ * ```
385
+ */
386
+ distinct() {
387
+ return new BaseQueryBuilder({
388
+ ...this.query,
389
+ distinct: true
390
+ });
391
+ }
372
392
  // Helper methods
373
393
  _getCurrentAliases() {
374
394
  const aliases = [];
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../src/query/builder/index.ts"],"sourcesContent":["import { CollectionImpl } from \"../../collection.js\"\nimport { CollectionRef, QueryRef } from \"../ir.js\"\nimport { createRefProxy, isRefProxy, toExpression } from \"./ref-proxy.js\"\nimport type { NamespacedRow } from \"../../types.js\"\nimport type {\n Aggregate,\n BasicExpression,\n JoinClause,\n OrderBy,\n OrderByClause,\n OrderByDirection,\n QueryIR,\n} from \"../ir.js\"\nimport type {\n Context,\n GroupByCallback,\n JoinOnCallback,\n MergeContext,\n MergeContextWithJoinType,\n OrderByCallback,\n RefProxyForContext,\n ResultTypeFromSelect,\n SchemaFromSource,\n SelectObject,\n Source,\n WhereCallback,\n WithResult,\n} from \"./types.js\"\n\nexport class BaseQueryBuilder<TContext extends Context = Context> {\n private readonly query: Partial<QueryIR> = {}\n\n constructor(query: Partial<QueryIR> = {}) {\n this.query = { ...query }\n }\n\n /**\n * Creates a CollectionRef or QueryRef from a source object\n * @param source - An object with a single key-value pair\n * @param context - Context string for error messages (e.g., \"from clause\", \"join clause\")\n * @returns A tuple of [alias, ref] where alias is the source key and ref is the created reference\n */\n private _createRefForSource<TSource extends Source>(\n source: TSource,\n context: string\n ): [string, CollectionRef | QueryRef] {\n if (Object.keys(source).length !== 1) {\n throw new Error(`Only one source is allowed in the ${context}`)\n }\n\n const alias = Object.keys(source)[0]!\n const sourceValue = source[alias]\n\n let ref: CollectionRef | QueryRef\n\n if (sourceValue instanceof CollectionImpl) {\n ref = new CollectionRef(sourceValue, alias)\n } else if (sourceValue instanceof BaseQueryBuilder) {\n const subQuery = sourceValue._getQuery()\n if (!(subQuery as Partial<QueryIR>).from) {\n throw new Error(\n `A sub query passed to a ${context} must have a from clause itself`\n )\n }\n ref = new QueryRef(subQuery, alias)\n } else {\n throw new Error(`Invalid source`)\n }\n\n return [alias, ref]\n }\n\n /**\n * Specify the source table or subquery for the query\n *\n * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery\n * @returns A QueryBuilder with the specified source\n *\n * @example\n * ```ts\n * // Query from a collection\n * query.from({ users: usersCollection })\n *\n * // Query from a subquery\n * const activeUsers = query.from({ u: usersCollection }).where(({u}) => u.active)\n * query.from({ activeUsers })\n * ```\n */\n from<TSource extends Source>(\n source: TSource\n ): QueryBuilder<{\n baseSchema: SchemaFromSource<TSource>\n schema: SchemaFromSource<TSource>\n fromSourceName: keyof TSource & string\n hasJoins: false\n }> {\n const [, from] = this._createRefForSource(source, `from clause`)\n\n return new BaseQueryBuilder({\n ...this.query,\n from,\n }) as any\n }\n\n /**\n * Join another table or subquery to the current query\n *\n * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery\n * @param onCallback - A function that receives table references and returns the join condition\n * @param type - The type of join: 'inner', 'left', 'right', or 'full' (defaults to 'left')\n * @returns A QueryBuilder with the joined table available\n *\n * @example\n * ```ts\n * // Left join users with posts\n * query\n * .from({ users: usersCollection })\n * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))\n *\n * // Inner join with explicit type\n * query\n * .from({ u: usersCollection })\n * .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId), 'inner')\n * ```\n *\n * // Join with a subquery\n * const activeUsers = query.from({ u: usersCollection }).where(({u}) => u.active)\n * query\n * .from({ activeUsers })\n * .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId))\n */\n join<\n TSource extends Source,\n TJoinType extends `inner` | `left` | `right` | `full` = `left`,\n >(\n source: TSource,\n onCallback: JoinOnCallback<\n MergeContext<TContext, SchemaFromSource<TSource>>\n >,\n type: TJoinType = `left` as TJoinType\n ): QueryBuilder<\n MergeContextWithJoinType<TContext, SchemaFromSource<TSource>, TJoinType>\n > {\n const [alias, from] = this._createRefForSource(source, `join clause`)\n\n // Create a temporary context for the callback\n const currentAliases = this._getCurrentAliases()\n const newAliases = [...currentAliases, alias]\n const refProxy = createRefProxy(newAliases) as RefProxyForContext<\n MergeContext<TContext, SchemaFromSource<TSource>>\n >\n\n // Get the join condition expression\n const onExpression = onCallback(refProxy)\n\n // Extract left and right from the expression\n // For now, we'll assume it's an eq function with two arguments\n let left: BasicExpression\n let right: BasicExpression\n\n if (\n onExpression.type === `func` &&\n onExpression.name === `eq` &&\n onExpression.args.length === 2\n ) {\n left = onExpression.args[0]!\n right = onExpression.args[1]!\n } else {\n throw new Error(`Join condition must be an equality expression`)\n }\n\n const joinClause: JoinClause = {\n from,\n type,\n left,\n right,\n }\n\n const existingJoins = this.query.join || []\n\n return new BaseQueryBuilder({\n ...this.query,\n join: [...existingJoins, joinClause],\n }) as any\n }\n\n /**\n * Filter rows based on a condition\n *\n * @param callback - A function that receives table references and returns an expression\n * @returns A QueryBuilder with the where condition applied\n *\n * @example\n * ```ts\n * // Simple condition\n * query\n * .from({ users: usersCollection })\n * .where(({users}) => gt(users.age, 18))\n *\n * // Multiple conditions\n * query\n * .from({ users: usersCollection })\n * .where(({users}) => and(\n * gt(users.age, 18),\n * eq(users.active, true)\n * ))\n *\n * // Multiple where calls are ANDed together\n * query\n * .from({ users: usersCollection })\n * .where(({users}) => gt(users.age, 18))\n * .where(({users}) => eq(users.active, true))\n * ```\n */\n where(callback: WhereCallback<TContext>): QueryBuilder<TContext> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefProxyForContext<TContext>\n const expression = callback(refProxy)\n\n const existingWhere = this.query.where || []\n\n return new BaseQueryBuilder({\n ...this.query,\n where: [...existingWhere, expression],\n }) as any\n }\n\n /**\n * Filter grouped rows based on aggregate conditions\n *\n * @param callback - A function that receives table references and returns an expression\n * @returns A QueryBuilder with the having condition applied\n *\n * @example\n * ```ts\n * // Filter groups by count\n * query\n * .from({ posts: postsCollection })\n * .groupBy(({posts}) => posts.userId)\n * .having(({posts}) => gt(count(posts.id), 5))\n *\n * // Filter by average\n * query\n * .from({ orders: ordersCollection })\n * .groupBy(({orders}) => orders.customerId)\n * .having(({orders}) => gt(avg(orders.total), 100))\n *\n * // Multiple having calls are ANDed together\n * query\n * .from({ orders: ordersCollection })\n * .groupBy(({orders}) => orders.customerId)\n * .having(({orders}) => gt(count(orders.id), 5))\n * .having(({orders}) => gt(avg(orders.total), 100))\n * ```\n */\n having(callback: WhereCallback<TContext>): QueryBuilder<TContext> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefProxyForContext<TContext>\n const expression = callback(refProxy)\n\n const existingHaving = this.query.having || []\n\n return new BaseQueryBuilder({\n ...this.query,\n having: [...existingHaving, expression],\n }) as any\n }\n\n /**\n * Select specific columns or computed values from the query\n *\n * @param callback - A function that receives table references and returns an object with selected fields or expressions\n * @returns A QueryBuilder that returns only the selected fields\n *\n * @example\n * ```ts\n * // Select specific columns\n * query\n * .from({ users: usersCollection })\n * .select(({users}) => ({\n * name: users.name,\n * email: users.email\n * }))\n *\n * // Select with computed values\n * query\n * .from({ users: usersCollection })\n * .select(({users}) => ({\n * fullName: concat(users.firstName, ' ', users.lastName),\n * ageInMonths: mul(users.age, 12)\n * }))\n *\n * // Select with aggregates (requires GROUP BY)\n * query\n * .from({ posts: postsCollection })\n * .groupBy(({posts}) => posts.userId)\n * .select(({posts, count}) => ({\n * userId: posts.userId,\n * postCount: count(posts.id)\n * }))\n * ```\n */\n select<TSelectObject extends SelectObject>(\n callback: (refs: RefProxyForContext<TContext>) => TSelectObject\n ): QueryBuilder<WithResult<TContext, ResultTypeFromSelect<TSelectObject>>> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefProxyForContext<TContext>\n const selectObject = callback(refProxy)\n\n // Check if any tables were spread during the callback\n const spreadSentinels = (refProxy as any).__spreadSentinels as Set<string>\n\n // Convert the select object to use expressions, including spread sentinels\n const select: Record<string, BasicExpression | Aggregate> = {}\n\n // First, add spread sentinels for any tables that were spread\n for (const spreadAlias of spreadSentinels) {\n const sentinelKey = `__SPREAD_SENTINEL__${spreadAlias}`\n select[sentinelKey] = toExpression(spreadAlias) // Use alias as a simple reference\n }\n\n // Then add the explicit select fields\n for (const [key, value] of Object.entries(selectObject)) {\n if (isRefProxy(value)) {\n select[key] = toExpression(value)\n } else if (\n typeof value === `object` &&\n `type` in value &&\n (value.type === `agg` || value.type === `func`)\n ) {\n select[key] = value as BasicExpression | Aggregate\n } else {\n select[key] = toExpression(value)\n }\n }\n\n return new BaseQueryBuilder({\n ...this.query,\n select,\n fnSelect: undefined, // remove the fnSelect clause if it exists\n }) as any\n }\n\n /**\n * Sort the query results by one or more columns\n *\n * @param callback - A function that receives table references and returns the field to sort by\n * @param direction - Sort direction: 'asc' for ascending, 'desc' for descending (defaults to 'asc')\n * @returns A QueryBuilder with the ordering applied\n *\n * @example\n * ```ts\n * // Sort by a single column\n * query\n * .from({ users: usersCollection })\n * .orderBy(({users}) => users.name)\n *\n * // Sort descending\n * query\n * .from({ users: usersCollection })\n * .orderBy(({users}) => users.createdAt, 'desc')\n *\n * // Multiple sorts (chain orderBy calls)\n * query\n * .from({ users: usersCollection })\n * .orderBy(({users}) => users.lastName)\n * .orderBy(({users}) => users.firstName)\n * ```\n */\n orderBy(\n callback: OrderByCallback<TContext>,\n direction: OrderByDirection = `asc`\n ): QueryBuilder<TContext> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefProxyForContext<TContext>\n const result = callback(refProxy)\n\n // Create the new OrderBy structure with expression and direction\n const orderByClause: OrderByClause = {\n expression: toExpression(result),\n direction,\n }\n\n const existingOrderBy: OrderBy = this.query.orderBy || []\n\n return new BaseQueryBuilder({\n ...this.query,\n orderBy: [...existingOrderBy, orderByClause],\n }) as any\n }\n\n /**\n * Group rows by one or more columns for aggregation\n *\n * @param callback - A function that receives table references and returns the field(s) to group by\n * @returns A QueryBuilder with grouping applied (enables aggregate functions in SELECT and HAVING)\n *\n * @example\n * ```ts\n * // Group by a single column\n * query\n * .from({ posts: postsCollection })\n * .groupBy(({posts}) => posts.userId)\n * .select(({posts, count}) => ({\n * userId: posts.userId,\n * postCount: count()\n * }))\n *\n * // Group by multiple columns\n * query\n * .from({ sales: salesCollection })\n * .groupBy(({sales}) => [sales.region, sales.category])\n * .select(({sales, sum}) => ({\n * region: sales.region,\n * category: sales.category,\n * totalSales: sum(sales.amount)\n * }))\n * ```\n */\n groupBy(callback: GroupByCallback<TContext>): QueryBuilder<TContext> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefProxyForContext<TContext>\n const result = callback(refProxy)\n\n const newExpressions = Array.isArray(result)\n ? result.map((r) => toExpression(r))\n : [toExpression(result)]\n\n // Replace existing groupBy expressions instead of extending them\n return new BaseQueryBuilder({\n ...this.query,\n groupBy: newExpressions,\n }) as any\n }\n\n /**\n * Limit the number of rows returned by the query\n * `orderBy` is required for `limit`\n *\n * @param count - Maximum number of rows to return\n * @returns A QueryBuilder with the limit applied\n *\n * @example\n * ```ts\n * // Get top 5 posts by likes\n * query\n * .from({ posts: postsCollection })\n * .orderBy(({posts}) => posts.likes, 'desc')\n * .limit(5)\n * ```\n */\n limit(count: number): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...this.query,\n limit: count,\n }) as any\n }\n\n /**\n * Skip a number of rows before returning results\n * `orderBy` is required for `offset`\n *\n * @param count - Number of rows to skip\n * @returns A QueryBuilder with the offset applied\n *\n * @example\n * ```ts\n * // Get second page of results\n * query\n * .from({ posts: postsCollection })\n * .orderBy(({posts}) => posts.createdAt, 'desc')\n * .offset(page * pageSize)\n * .limit(pageSize)\n * ```\n */\n offset(count: number): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...this.query,\n offset: count,\n }) as any\n }\n\n // Helper methods\n private _getCurrentAliases(): Array<string> {\n const aliases: Array<string> = []\n\n // Add the from alias\n if (this.query.from) {\n aliases.push(this.query.from.alias)\n }\n\n // Add join aliases\n if (this.query.join) {\n for (const join of this.query.join) {\n aliases.push(join.from.alias)\n }\n }\n\n return aliases\n }\n\n /**\n * Functional variants of the query builder\n * These are imperative function that are called for ery row.\n * Warning: that these cannot be optimized by the query compiler, and may prevent\n * some type of optimizations being possible.\n * @example\n * ```ts\n * q.fn.select((row) => ({\n * name: row.user.name.toUpperCase(),\n * age: row.user.age + 1,\n * }))\n * ```\n */\n get fn() {\n const builder = this\n return {\n /**\n * Select fields using a function that operates on each row\n * Warning: This cannot be optimized by the query compiler\n *\n * @param callback - A function that receives a row and returns the selected value\n * @returns A QueryBuilder with functional selection applied\n *\n * @example\n * ```ts\n * // Functional select (not optimized)\n * query\n * .from({ users: usersCollection })\n * .fn.select(row => ({\n * name: row.users.name.toUpperCase(),\n * age: row.users.age + 1,\n * }))\n * ```\n */\n select<TFuncSelectResult>(\n callback: (row: TContext[`schema`]) => TFuncSelectResult\n ): QueryBuilder<WithResult<TContext, TFuncSelectResult>> {\n return new BaseQueryBuilder({\n ...builder.query,\n select: undefined, // remove the select clause if it exists\n fnSelect: callback,\n })\n },\n /**\n * Filter rows using a function that operates on each row\n * Warning: This cannot be optimized by the query compiler\n *\n * @param callback - A function that receives a row and returns a boolean\n * @returns A QueryBuilder with functional filtering applied\n *\n * @example\n * ```ts\n * // Functional where (not optimized)\n * query\n * .from({ users: usersCollection })\n * .fn.where(row => row.users.name.startsWith('A'))\n * ```\n */\n where(\n callback: (row: TContext[`schema`]) => any\n ): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...builder.query,\n fnWhere: [\n ...(builder.query.fnWhere || []),\n callback as (row: NamespacedRow) => any,\n ],\n })\n },\n /**\n * Filter grouped rows using a function that operates on each aggregated row\n * Warning: This cannot be optimized by the query compiler\n *\n * @param callback - A function that receives an aggregated row and returns a boolean\n * @returns A QueryBuilder with functional having filter applied\n *\n * @example\n * ```ts\n * // Functional having (not optimized)\n * query\n * .from({ posts: postsCollection })\n * .groupBy(({posts}) => posts.userId)\n * .fn.having(row => row.count > 5)\n * ```\n */\n having(\n callback: (row: TContext[`schema`]) => any\n ): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...builder.query,\n fnHaving: [\n ...(builder.query.fnHaving || []),\n callback as (row: NamespacedRow) => any,\n ],\n })\n },\n }\n }\n\n _getQuery(): QueryIR {\n if (!this.query.from) {\n throw new Error(`Query must have a from clause`)\n }\n return this.query as QueryIR\n }\n}\n\n// Internal function to build a query from a callback\n// used by liveQueryCollectionOptions.query\nexport function buildQuery<TContext extends Context>(\n fn: (builder: InitialQueryBuilder) => QueryBuilder<TContext>\n): QueryIR {\n const result = fn(new BaseQueryBuilder())\n return getQueryIR(result)\n}\n\n// Internal function to get the QueryIR from a builder\nexport function getQueryIR(\n builder: BaseQueryBuilder | QueryBuilder<any> | InitialQueryBuilder\n): QueryIR {\n return (builder as unknown as BaseQueryBuilder)._getQuery()\n}\n\n// Type-only exports for the query builder\nexport type InitialQueryBuilder = Pick<BaseQueryBuilder<Context>, `from`>\n\nexport type InitialQueryBuilderConstructor = new () => InitialQueryBuilder\n\nexport type QueryBuilder<TContext extends Context> = Omit<\n BaseQueryBuilder<TContext>,\n `from` | `_getQuery`\n>\n\n// Main query builder class alias with the constructor type modified to hide all\n// but the from method on the initial instance\nexport const Query: InitialQueryBuilderConstructor = BaseQueryBuilder\n\n// Helper type to extract context from a QueryBuilder\nexport type ExtractContext<T> =\n T extends BaseQueryBuilder<infer TContext>\n ? TContext\n : T extends QueryBuilder<infer TContext>\n ? TContext\n : never\n\n// Export the types from types.ts for convenience\nexport type { Context, Source, GetResult } from \"./types.js\"\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../../../src/query/builder/index.ts"],"sourcesContent":["import { CollectionImpl } from \"../../collection.js\"\nimport { CollectionRef, QueryRef } from \"../ir.js\"\nimport { createRefProxy, isRefProxy, toExpression } from \"./ref-proxy.js\"\nimport type { NamespacedRow } from \"../../types.js\"\nimport type {\n Aggregate,\n BasicExpression,\n JoinClause,\n OrderBy,\n OrderByClause,\n OrderByDirection,\n QueryIR,\n} from \"../ir.js\"\nimport type {\n Context,\n GroupByCallback,\n JoinOnCallback,\n MergeContext,\n MergeContextWithJoinType,\n OrderByCallback,\n RefProxyForContext,\n ResultTypeFromSelect,\n SchemaFromSource,\n SelectObject,\n Source,\n WhereCallback,\n WithResult,\n} from \"./types.js\"\n\nexport class BaseQueryBuilder<TContext extends Context = Context> {\n private readonly query: Partial<QueryIR> = {}\n\n constructor(query: Partial<QueryIR> = {}) {\n this.query = { ...query }\n }\n\n /**\n * Creates a CollectionRef or QueryRef from a source object\n * @param source - An object with a single key-value pair\n * @param context - Context string for error messages (e.g., \"from clause\", \"join clause\")\n * @returns A tuple of [alias, ref] where alias is the source key and ref is the created reference\n */\n private _createRefForSource<TSource extends Source>(\n source: TSource,\n context: string\n ): [string, CollectionRef | QueryRef] {\n if (Object.keys(source).length !== 1) {\n throw new Error(`Only one source is allowed in the ${context}`)\n }\n\n const alias = Object.keys(source)[0]!\n const sourceValue = source[alias]\n\n let ref: CollectionRef | QueryRef\n\n if (sourceValue instanceof CollectionImpl) {\n ref = new CollectionRef(sourceValue, alias)\n } else if (sourceValue instanceof BaseQueryBuilder) {\n const subQuery = sourceValue._getQuery()\n if (!(subQuery as Partial<QueryIR>).from) {\n throw new Error(\n `A sub query passed to a ${context} must have a from clause itself`\n )\n }\n ref = new QueryRef(subQuery, alias)\n } else {\n throw new Error(`Invalid source`)\n }\n\n return [alias, ref]\n }\n\n /**\n * Specify the source table or subquery for the query\n *\n * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery\n * @returns A QueryBuilder with the specified source\n *\n * @example\n * ```ts\n * // Query from a collection\n * query.from({ users: usersCollection })\n *\n * // Query from a subquery\n * const activeUsers = query.from({ u: usersCollection }).where(({u}) => u.active)\n * query.from({ activeUsers })\n * ```\n */\n from<TSource extends Source>(\n source: TSource\n ): QueryBuilder<{\n baseSchema: SchemaFromSource<TSource>\n schema: SchemaFromSource<TSource>\n fromSourceName: keyof TSource & string\n hasJoins: false\n }> {\n const [, from] = this._createRefForSource(source, `from clause`)\n\n return new BaseQueryBuilder({\n ...this.query,\n from,\n }) as any\n }\n\n /**\n * Join another table or subquery to the current query\n *\n * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery\n * @param onCallback - A function that receives table references and returns the join condition\n * @param type - The type of join: 'inner', 'left', 'right', or 'full' (defaults to 'left')\n * @returns A QueryBuilder with the joined table available\n *\n * @example\n * ```ts\n * // Left join users with posts\n * query\n * .from({ users: usersCollection })\n * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))\n *\n * // Inner join with explicit type\n * query\n * .from({ u: usersCollection })\n * .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId), 'inner')\n * ```\n *\n * // Join with a subquery\n * const activeUsers = query.from({ u: usersCollection }).where(({u}) => u.active)\n * query\n * .from({ activeUsers })\n * .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId))\n */\n join<\n TSource extends Source,\n TJoinType extends `inner` | `left` | `right` | `full` = `left`,\n >(\n source: TSource,\n onCallback: JoinOnCallback<\n MergeContext<TContext, SchemaFromSource<TSource>>\n >,\n type: TJoinType = `left` as TJoinType\n ): QueryBuilder<\n MergeContextWithJoinType<TContext, SchemaFromSource<TSource>, TJoinType>\n > {\n const [alias, from] = this._createRefForSource(source, `join clause`)\n\n // Create a temporary context for the callback\n const currentAliases = this._getCurrentAliases()\n const newAliases = [...currentAliases, alias]\n const refProxy = createRefProxy(newAliases) as RefProxyForContext<\n MergeContext<TContext, SchemaFromSource<TSource>>\n >\n\n // Get the join condition expression\n const onExpression = onCallback(refProxy)\n\n // Extract left and right from the expression\n // For now, we'll assume it's an eq function with two arguments\n let left: BasicExpression\n let right: BasicExpression\n\n if (\n onExpression.type === `func` &&\n onExpression.name === `eq` &&\n onExpression.args.length === 2\n ) {\n left = onExpression.args[0]!\n right = onExpression.args[1]!\n } else {\n throw new Error(`Join condition must be an equality expression`)\n }\n\n const joinClause: JoinClause = {\n from,\n type,\n left,\n right,\n }\n\n const existingJoins = this.query.join || []\n\n return new BaseQueryBuilder({\n ...this.query,\n join: [...existingJoins, joinClause],\n }) as any\n }\n\n /**\n * Filter rows based on a condition\n *\n * @param callback - A function that receives table references and returns an expression\n * @returns A QueryBuilder with the where condition applied\n *\n * @example\n * ```ts\n * // Simple condition\n * query\n * .from({ users: usersCollection })\n * .where(({users}) => gt(users.age, 18))\n *\n * // Multiple conditions\n * query\n * .from({ users: usersCollection })\n * .where(({users}) => and(\n * gt(users.age, 18),\n * eq(users.active, true)\n * ))\n *\n * // Multiple where calls are ANDed together\n * query\n * .from({ users: usersCollection })\n * .where(({users}) => gt(users.age, 18))\n * .where(({users}) => eq(users.active, true))\n * ```\n */\n where(callback: WhereCallback<TContext>): QueryBuilder<TContext> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefProxyForContext<TContext>\n const expression = callback(refProxy)\n\n const existingWhere = this.query.where || []\n\n return new BaseQueryBuilder({\n ...this.query,\n where: [...existingWhere, expression],\n }) as any\n }\n\n /**\n * Filter grouped rows based on aggregate conditions\n *\n * @param callback - A function that receives table references and returns an expression\n * @returns A QueryBuilder with the having condition applied\n *\n * @example\n * ```ts\n * // Filter groups by count\n * query\n * .from({ posts: postsCollection })\n * .groupBy(({posts}) => posts.userId)\n * .having(({posts}) => gt(count(posts.id), 5))\n *\n * // Filter by average\n * query\n * .from({ orders: ordersCollection })\n * .groupBy(({orders}) => orders.customerId)\n * .having(({orders}) => gt(avg(orders.total), 100))\n *\n * // Multiple having calls are ANDed together\n * query\n * .from({ orders: ordersCollection })\n * .groupBy(({orders}) => orders.customerId)\n * .having(({orders}) => gt(count(orders.id), 5))\n * .having(({orders}) => gt(avg(orders.total), 100))\n * ```\n */\n having(callback: WhereCallback<TContext>): QueryBuilder<TContext> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefProxyForContext<TContext>\n const expression = callback(refProxy)\n\n const existingHaving = this.query.having || []\n\n return new BaseQueryBuilder({\n ...this.query,\n having: [...existingHaving, expression],\n }) as any\n }\n\n /**\n * Select specific columns or computed values from the query\n *\n * @param callback - A function that receives table references and returns an object with selected fields or expressions\n * @returns A QueryBuilder that returns only the selected fields\n *\n * @example\n * ```ts\n * // Select specific columns\n * query\n * .from({ users: usersCollection })\n * .select(({users}) => ({\n * name: users.name,\n * email: users.email\n * }))\n *\n * // Select with computed values\n * query\n * .from({ users: usersCollection })\n * .select(({users}) => ({\n * fullName: concat(users.firstName, ' ', users.lastName),\n * ageInMonths: mul(users.age, 12)\n * }))\n *\n * // Select with aggregates (requires GROUP BY)\n * query\n * .from({ posts: postsCollection })\n * .groupBy(({posts}) => posts.userId)\n * .select(({posts, count}) => ({\n * userId: posts.userId,\n * postCount: count(posts.id)\n * }))\n * ```\n */\n select<TSelectObject extends SelectObject>(\n callback: (refs: RefProxyForContext<TContext>) => TSelectObject\n ): QueryBuilder<WithResult<TContext, ResultTypeFromSelect<TSelectObject>>> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefProxyForContext<TContext>\n const selectObject = callback(refProxy)\n\n // Check if any tables were spread during the callback\n const spreadSentinels = (refProxy as any).__spreadSentinels as Set<string>\n\n // Convert the select object to use expressions, including spread sentinels\n const select: Record<string, BasicExpression | Aggregate> = {}\n\n // First, add spread sentinels for any tables that were spread\n for (const spreadAlias of spreadSentinels) {\n const sentinelKey = `__SPREAD_SENTINEL__${spreadAlias}`\n select[sentinelKey] = toExpression(spreadAlias) // Use alias as a simple reference\n }\n\n // Then add the explicit select fields\n for (const [key, value] of Object.entries(selectObject)) {\n if (isRefProxy(value)) {\n select[key] = toExpression(value)\n } else if (\n typeof value === `object` &&\n `type` in value &&\n (value.type === `agg` || value.type === `func`)\n ) {\n select[key] = value as BasicExpression | Aggregate\n } else {\n select[key] = toExpression(value)\n }\n }\n\n return new BaseQueryBuilder({\n ...this.query,\n select,\n fnSelect: undefined, // remove the fnSelect clause if it exists\n }) as any\n }\n\n /**\n * Sort the query results by one or more columns\n *\n * @param callback - A function that receives table references and returns the field to sort by\n * @param direction - Sort direction: 'asc' for ascending, 'desc' for descending (defaults to 'asc')\n * @returns A QueryBuilder with the ordering applied\n *\n * @example\n * ```ts\n * // Sort by a single column\n * query\n * .from({ users: usersCollection })\n * .orderBy(({users}) => users.name)\n *\n * // Sort descending\n * query\n * .from({ users: usersCollection })\n * .orderBy(({users}) => users.createdAt, 'desc')\n *\n * // Multiple sorts (chain orderBy calls)\n * query\n * .from({ users: usersCollection })\n * .orderBy(({users}) => users.lastName)\n * .orderBy(({users}) => users.firstName)\n * ```\n */\n orderBy(\n callback: OrderByCallback<TContext>,\n direction: OrderByDirection = `asc`\n ): QueryBuilder<TContext> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefProxyForContext<TContext>\n const result = callback(refProxy)\n\n // Create the new OrderBy structure with expression and direction\n const orderByClause: OrderByClause = {\n expression: toExpression(result),\n direction,\n }\n\n const existingOrderBy: OrderBy = this.query.orderBy || []\n\n return new BaseQueryBuilder({\n ...this.query,\n orderBy: [...existingOrderBy, orderByClause],\n }) as any\n }\n\n /**\n * Group rows by one or more columns for aggregation\n *\n * @param callback - A function that receives table references and returns the field(s) to group by\n * @returns A QueryBuilder with grouping applied (enables aggregate functions in SELECT and HAVING)\n *\n * @example\n * ```ts\n * // Group by a single column\n * query\n * .from({ posts: postsCollection })\n * .groupBy(({posts}) => posts.userId)\n * .select(({posts, count}) => ({\n * userId: posts.userId,\n * postCount: count()\n * }))\n *\n * // Group by multiple columns\n * query\n * .from({ sales: salesCollection })\n * .groupBy(({sales}) => [sales.region, sales.category])\n * .select(({sales, sum}) => ({\n * region: sales.region,\n * category: sales.category,\n * totalSales: sum(sales.amount)\n * }))\n * ```\n */\n groupBy(callback: GroupByCallback<TContext>): QueryBuilder<TContext> {\n const aliases = this._getCurrentAliases()\n const refProxy = createRefProxy(aliases) as RefProxyForContext<TContext>\n const result = callback(refProxy)\n\n const newExpressions = Array.isArray(result)\n ? result.map((r) => toExpression(r))\n : [toExpression(result)]\n\n // Replace existing groupBy expressions instead of extending them\n return new BaseQueryBuilder({\n ...this.query,\n groupBy: newExpressions,\n }) as any\n }\n\n /**\n * Limit the number of rows returned by the query\n * `orderBy` is required for `limit`\n *\n * @param count - Maximum number of rows to return\n * @returns A QueryBuilder with the limit applied\n *\n * @example\n * ```ts\n * // Get top 5 posts by likes\n * query\n * .from({ posts: postsCollection })\n * .orderBy(({posts}) => posts.likes, 'desc')\n * .limit(5)\n * ```\n */\n limit(count: number): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...this.query,\n limit: count,\n }) as any\n }\n\n /**\n * Skip a number of rows before returning results\n * `orderBy` is required for `offset`\n *\n * @param count - Number of rows to skip\n * @returns A QueryBuilder with the offset applied\n *\n * @example\n * ```ts\n * // Get second page of results\n * query\n * .from({ posts: postsCollection })\n * .orderBy(({posts}) => posts.createdAt, 'desc')\n * .offset(page * pageSize)\n * .limit(pageSize)\n * ```\n */\n offset(count: number): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...this.query,\n offset: count,\n }) as any\n }\n\n /**\n * Specify that the query should return distinct rows.\n * Deduplicates rows based on the selected columns.\n * @returns A QueryBuilder with distinct enabled\n *\n * @example\n * ```ts\n * // Get countries our users are from\n * query\n * .from({ users: usersCollection })\n * .select(({users}) => users.country)\n * .distinct()\n * ```\n */\n distinct(): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...this.query,\n distinct: true,\n }) as any\n }\n\n // Helper methods\n private _getCurrentAliases(): Array<string> {\n const aliases: Array<string> = []\n\n // Add the from alias\n if (this.query.from) {\n aliases.push(this.query.from.alias)\n }\n\n // Add join aliases\n if (this.query.join) {\n for (const join of this.query.join) {\n aliases.push(join.from.alias)\n }\n }\n\n return aliases\n }\n\n /**\n * Functional variants of the query builder\n * These are imperative function that are called for ery row.\n * Warning: that these cannot be optimized by the query compiler, and may prevent\n * some type of optimizations being possible.\n * @example\n * ```ts\n * q.fn.select((row) => ({\n * name: row.user.name.toUpperCase(),\n * age: row.user.age + 1,\n * }))\n * ```\n */\n get fn() {\n const builder = this\n return {\n /**\n * Select fields using a function that operates on each row\n * Warning: This cannot be optimized by the query compiler\n *\n * @param callback - A function that receives a row and returns the selected value\n * @returns A QueryBuilder with functional selection applied\n *\n * @example\n * ```ts\n * // Functional select (not optimized)\n * query\n * .from({ users: usersCollection })\n * .fn.select(row => ({\n * name: row.users.name.toUpperCase(),\n * age: row.users.age + 1,\n * }))\n * ```\n */\n select<TFuncSelectResult>(\n callback: (row: TContext[`schema`]) => TFuncSelectResult\n ): QueryBuilder<WithResult<TContext, TFuncSelectResult>> {\n return new BaseQueryBuilder({\n ...builder.query,\n select: undefined, // remove the select clause if it exists\n fnSelect: callback,\n })\n },\n /**\n * Filter rows using a function that operates on each row\n * Warning: This cannot be optimized by the query compiler\n *\n * @param callback - A function that receives a row and returns a boolean\n * @returns A QueryBuilder with functional filtering applied\n *\n * @example\n * ```ts\n * // Functional where (not optimized)\n * query\n * .from({ users: usersCollection })\n * .fn.where(row => row.users.name.startsWith('A'))\n * ```\n */\n where(\n callback: (row: TContext[`schema`]) => any\n ): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...builder.query,\n fnWhere: [\n ...(builder.query.fnWhere || []),\n callback as (row: NamespacedRow) => any,\n ],\n })\n },\n /**\n * Filter grouped rows using a function that operates on each aggregated row\n * Warning: This cannot be optimized by the query compiler\n *\n * @param callback - A function that receives an aggregated row and returns a boolean\n * @returns A QueryBuilder with functional having filter applied\n *\n * @example\n * ```ts\n * // Functional having (not optimized)\n * query\n * .from({ posts: postsCollection })\n * .groupBy(({posts}) => posts.userId)\n * .fn.having(row => row.count > 5)\n * ```\n */\n having(\n callback: (row: TContext[`schema`]) => any\n ): QueryBuilder<TContext> {\n return new BaseQueryBuilder({\n ...builder.query,\n fnHaving: [\n ...(builder.query.fnHaving || []),\n callback as (row: NamespacedRow) => any,\n ],\n })\n },\n }\n }\n\n _getQuery(): QueryIR {\n if (!this.query.from) {\n throw new Error(`Query must have a from clause`)\n }\n return this.query as QueryIR\n }\n}\n\n// Internal function to build a query from a callback\n// used by liveQueryCollectionOptions.query\nexport function buildQuery<TContext extends Context>(\n fn: (builder: InitialQueryBuilder) => QueryBuilder<TContext>\n): QueryIR {\n const result = fn(new BaseQueryBuilder())\n return getQueryIR(result)\n}\n\n// Internal function to get the QueryIR from a builder\nexport function getQueryIR(\n builder: BaseQueryBuilder | QueryBuilder<any> | InitialQueryBuilder\n): QueryIR {\n return (builder as unknown as BaseQueryBuilder)._getQuery()\n}\n\n// Type-only exports for the query builder\nexport type InitialQueryBuilder = Pick<BaseQueryBuilder<Context>, `from`>\n\nexport type InitialQueryBuilderConstructor = new () => InitialQueryBuilder\n\nexport type QueryBuilder<TContext extends Context> = Omit<\n BaseQueryBuilder<TContext>,\n `from` | `_getQuery`\n>\n\n// Main query builder class alias with the constructor type modified to hide all\n// but the from method on the initial instance\nexport const Query: InitialQueryBuilderConstructor = BaseQueryBuilder\n\n// Helper type to extract context from a QueryBuilder\nexport type ExtractContext<T> =\n T extends BaseQueryBuilder<infer TContext>\n ? TContext\n : T extends QueryBuilder<infer TContext>\n ? TContext\n : never\n\n// Export the types from types.ts for convenience\nexport type { Context, Source, GetResult } from \"./types.js\"\n"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { map, filter } from "@electric-sql/d2mini";
1
+ import { map, filter, distinct } from "@electric-sql/d2mini";
2
2
  import { compileExpression } from "./evaluators.js";
3
3
  import { processJoins } from "./joins.js";
4
4
  import { processGroupBy } from "./group-by.js";
@@ -52,6 +52,9 @@ function compileQuery(query, inputs, cache = /* @__PURE__ */ new WeakMap()) {
52
52
  );
53
53
  }
54
54
  }
55
+ if (query.distinct && !query.fnSelect && !query.select) {
56
+ throw new Error(`DISTINCT requires a SELECT clause.`);
57
+ }
55
58
  if (query.fnSelect) {
56
59
  pipeline = pipeline.pipe(
57
60
  map(([key, namespacedRow]) => {
@@ -119,6 +122,9 @@ function compileQuery(query, inputs, cache = /* @__PURE__ */ new WeakMap()) {
119
122
  );
120
123
  }
121
124
  }
125
+ if (query.distinct) {
126
+ pipeline = pipeline.pipe(distinct(([_key, row]) => row.__select_results));
127
+ }
122
128
  if (query.orderBy && query.orderBy.length > 0) {
123
129
  const orderedPipeline = processOrderBy(
124
130
  pipeline,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../src/query/compiler/index.ts"],"sourcesContent":["import { filter, map } from \"@electric-sql/d2mini\"\nimport { compileExpression } from \"./evaluators.js\"\nimport { processJoins } from \"./joins.js\"\nimport { processGroupBy } from \"./group-by.js\"\nimport { processOrderBy } from \"./order-by.js\"\nimport { processSelectToResults } from \"./select.js\"\nimport type { CollectionRef, QueryIR, QueryRef } from \"../ir.js\"\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n ResultStream,\n} from \"../../types.js\"\n\n/**\n * Cache for compiled subqueries to avoid duplicate compilation\n */\ntype QueryCache = WeakMap<QueryIR, ResultStream>\n\n/**\n * Compiles a query2 IR into a D2 pipeline\n * @param query The query IR to compile\n * @param inputs Mapping of collection names to input streams\n * @param cache Optional cache for compiled subqueries (used internally for recursion)\n * @returns A stream builder representing the compiled query\n */\nexport function compileQuery(\n query: QueryIR,\n inputs: Record<string, KeyedStream>,\n cache: QueryCache = new WeakMap()\n): ResultStream {\n // Check if this query has already been compiled\n const cachedResult = cache.get(query)\n if (cachedResult) {\n return cachedResult\n }\n\n // Create a copy of the inputs map to avoid modifying the original\n const allInputs = { ...inputs }\n\n // Create a map of table aliases to inputs\n const tables: Record<string, KeyedStream> = {}\n\n // Process the FROM clause to get the main table\n const { alias: mainTableAlias, input: mainInput } = processFrom(\n query.from,\n allInputs,\n cache\n )\n tables[mainTableAlias] = mainInput\n\n // Prepare the initial pipeline with the main table wrapped in its alias\n let pipeline: NamespacedAndKeyedStream = mainInput.pipe(\n map(([key, row]) => {\n // Initialize the record with a nested structure\n const ret = [key, { [mainTableAlias]: row }] as [\n string,\n Record<string, typeof row>,\n ]\n return ret\n })\n )\n\n // Process JOIN clauses if they exist\n if (query.join && query.join.length > 0) {\n pipeline = processJoins(\n pipeline,\n query.join,\n tables,\n mainTableAlias,\n allInputs,\n cache\n )\n }\n\n // Process the WHERE clause if it exists\n if (query.where && query.where.length > 0) {\n // Compile all WHERE expressions\n const compiledWheres = query.where.map((where) => compileExpression(where))\n\n // Apply each WHERE condition as a filter (they are ANDed together)\n for (const compiledWhere of compiledWheres) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return compiledWhere(namespacedRow)\n })\n )\n }\n }\n\n // Process functional WHERE clauses if they exist\n if (query.fnWhere && query.fnWhere.length > 0) {\n for (const fnWhere of query.fnWhere) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return fnWhere(namespacedRow)\n })\n )\n }\n }\n\n // Process the SELECT clause early - always create __select_results\n // This eliminates duplication and allows for future DISTINCT implementation\n if (query.fnSelect) {\n // Handle functional select - apply the function to transform the row\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults = query.fnSelect!(namespacedRow)\n return [\n key,\n {\n ...namespacedRow,\n __select_results: selectResults,\n },\n ] as [string, typeof namespacedRow & { __select_results: any }]\n })\n )\n } else if (query.select) {\n pipeline = processSelectToResults(pipeline, query.select, allInputs)\n } else {\n // If no SELECT clause, create __select_results with the main table data\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults =\n !query.join && !query.groupBy\n ? namespacedRow[mainTableAlias]\n : namespacedRow\n\n return [\n key,\n {\n ...namespacedRow,\n __select_results: selectResults,\n },\n ] as [string, typeof namespacedRow & { __select_results: any }]\n })\n )\n }\n\n // Process the GROUP BY clause if it exists\n if (query.groupBy && query.groupBy.length > 0) {\n pipeline = processGroupBy(\n pipeline,\n query.groupBy,\n query.having,\n query.select,\n query.fnHaving\n )\n } else if (query.select) {\n // Check if SELECT contains aggregates but no GROUP BY (implicit single-group aggregation)\n const hasAggregates = Object.values(query.select).some(\n (expr) => expr.type === `agg`\n )\n if (hasAggregates) {\n // Handle implicit single-group aggregation\n pipeline = processGroupBy(\n pipeline,\n [], // Empty group by means single group\n query.having,\n query.select,\n query.fnHaving\n )\n }\n }\n\n // Process the HAVING clause if it exists (only applies after GROUP BY)\n if (query.having && (!query.groupBy || query.groupBy.length === 0)) {\n // Check if we have aggregates in SELECT that would trigger implicit grouping\n const hasAggregates = query.select\n ? Object.values(query.select).some((expr) => expr.type === `agg`)\n : false\n\n if (!hasAggregates) {\n throw new Error(`HAVING clause requires GROUP BY clause`)\n }\n }\n\n // Process functional HAVING clauses outside of GROUP BY (treat as additional WHERE filters)\n if (\n query.fnHaving &&\n query.fnHaving.length > 0 &&\n (!query.groupBy || query.groupBy.length === 0)\n ) {\n // If there's no GROUP BY but there are fnHaving clauses, apply them as filters\n for (const fnHaving of query.fnHaving) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return fnHaving(namespacedRow)\n })\n )\n }\n }\n\n // Process orderBy parameter if it exists\n if (query.orderBy && query.orderBy.length > 0) {\n const orderedPipeline = processOrderBy(\n pipeline,\n query.orderBy,\n query.limit,\n query.offset\n )\n\n // Final step: extract the __select_results and include orderBy index\n const resultPipeline = orderedPipeline.pipe(\n map(([key, [row, orderByIndex]]) => {\n // Extract the final results from __select_results and include orderBy index\n const finalResults = (row as any).__select_results\n return [key, [finalResults, orderByIndex]] as [unknown, [any, string]]\n })\n )\n\n const result = resultPipeline\n // Cache the result before returning\n cache.set(query, result)\n return result\n } else if (query.limit !== undefined || query.offset !== undefined) {\n // If there's a limit or offset without orderBy, throw an error\n throw new Error(\n `LIMIT and OFFSET require an ORDER BY clause to ensure deterministic results`\n )\n }\n\n // Final step: extract the __select_results and return tuple format (no orderBy)\n const resultPipeline: ResultStream = pipeline.pipe(\n map(([key, row]) => {\n // Extract the final results from __select_results and return [key, [results, undefined]]\n const finalResults = (row as any).__select_results\n return [key, [finalResults, undefined]] as [\n unknown,\n [any, string | undefined],\n ]\n })\n )\n\n const result = resultPipeline\n // Cache the result before returning\n cache.set(query, result)\n return result\n}\n\n/**\n * Processes the FROM clause to extract the main table alias and input stream\n */\nfunction processFrom(\n from: CollectionRef | QueryRef,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache\n): { alias: string; input: KeyedStream } {\n switch (from.type) {\n case `collectionRef`: {\n const input = allInputs[from.collection.id]\n if (!input) {\n throw new Error(\n `Input for collection \"${from.collection.id}\" not found in inputs map`\n )\n }\n return { alias: from.alias, input }\n }\n case `queryRef`: {\n // Recursively compile the sub-query with cache\n const subQueryInput = compileQuery(from.query, allInputs, cache)\n\n // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)\n // We need to extract just the value for use in parent queries\n const extractedInput = subQueryInput.pipe(\n map((data: any) => {\n const [key, [value, _orderByIndex]] = data\n return [key, value] as [unknown, any]\n })\n )\n\n return { alias: from.alias, input: extractedInput }\n }\n default:\n throw new Error(`Unsupported FROM type: ${(from as any).type}`)\n }\n}\n"],"names":["resultPipeline","result"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../../../src/query/compiler/index.ts"],"sourcesContent":["import { distinct, filter, map } from \"@electric-sql/d2mini\"\nimport { compileExpression } from \"./evaluators.js\"\nimport { processJoins } from \"./joins.js\"\nimport { processGroupBy } from \"./group-by.js\"\nimport { processOrderBy } from \"./order-by.js\"\nimport { processSelectToResults } from \"./select.js\"\nimport type { CollectionRef, QueryIR, QueryRef } from \"../ir.js\"\nimport type {\n KeyedStream,\n NamespacedAndKeyedStream,\n ResultStream,\n} from \"../../types.js\"\n\n/**\n * Cache for compiled subqueries to avoid duplicate compilation\n */\ntype QueryCache = WeakMap<QueryIR, ResultStream>\n\n/**\n * Compiles a query2 IR into a D2 pipeline\n * @param query The query IR to compile\n * @param inputs Mapping of collection names to input streams\n * @param cache Optional cache for compiled subqueries (used internally for recursion)\n * @returns A stream builder representing the compiled query\n */\nexport function compileQuery(\n query: QueryIR,\n inputs: Record<string, KeyedStream>,\n cache: QueryCache = new WeakMap()\n): ResultStream {\n // Check if this query has already been compiled\n const cachedResult = cache.get(query)\n if (cachedResult) {\n return cachedResult\n }\n\n // Create a copy of the inputs map to avoid modifying the original\n const allInputs = { ...inputs }\n\n // Create a map of table aliases to inputs\n const tables: Record<string, KeyedStream> = {}\n\n // Process the FROM clause to get the main table\n const { alias: mainTableAlias, input: mainInput } = processFrom(\n query.from,\n allInputs,\n cache\n )\n tables[mainTableAlias] = mainInput\n\n // Prepare the initial pipeline with the main table wrapped in its alias\n let pipeline: NamespacedAndKeyedStream = mainInput.pipe(\n map(([key, row]) => {\n // Initialize the record with a nested structure\n const ret = [key, { [mainTableAlias]: row }] as [\n string,\n Record<string, typeof row>,\n ]\n return ret\n })\n )\n\n // Process JOIN clauses if they exist\n if (query.join && query.join.length > 0) {\n pipeline = processJoins(\n pipeline,\n query.join,\n tables,\n mainTableAlias,\n allInputs,\n cache\n )\n }\n\n // Process the WHERE clause if it exists\n if (query.where && query.where.length > 0) {\n // Compile all WHERE expressions\n const compiledWheres = query.where.map((where) => compileExpression(where))\n\n // Apply each WHERE condition as a filter (they are ANDed together)\n for (const compiledWhere of compiledWheres) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return compiledWhere(namespacedRow)\n })\n )\n }\n }\n\n // Process functional WHERE clauses if they exist\n if (query.fnWhere && query.fnWhere.length > 0) {\n for (const fnWhere of query.fnWhere) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return fnWhere(namespacedRow)\n })\n )\n }\n }\n\n if (query.distinct && !query.fnSelect && !query.select) {\n throw new Error(`DISTINCT requires a SELECT clause.`)\n }\n\n // Process the SELECT clause early - always create __select_results\n // This eliminates duplication and allows for DISTINCT implementation\n if (query.fnSelect) {\n // Handle functional select - apply the function to transform the row\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults = query.fnSelect!(namespacedRow)\n return [\n key,\n {\n ...namespacedRow,\n __select_results: selectResults,\n },\n ] as [string, typeof namespacedRow & { __select_results: any }]\n })\n )\n } else if (query.select) {\n pipeline = processSelectToResults(pipeline, query.select, allInputs)\n } else {\n // If no SELECT clause, create __select_results with the main table data\n pipeline = pipeline.pipe(\n map(([key, namespacedRow]) => {\n const selectResults =\n !query.join && !query.groupBy\n ? namespacedRow[mainTableAlias]\n : namespacedRow\n\n return [\n key,\n {\n ...namespacedRow,\n __select_results: selectResults,\n },\n ] as [string, typeof namespacedRow & { __select_results: any }]\n })\n )\n }\n\n // Process the GROUP BY clause if it exists\n if (query.groupBy && query.groupBy.length > 0) {\n pipeline = processGroupBy(\n pipeline,\n query.groupBy,\n query.having,\n query.select,\n query.fnHaving\n )\n } else if (query.select) {\n // Check if SELECT contains aggregates but no GROUP BY (implicit single-group aggregation)\n const hasAggregates = Object.values(query.select).some(\n (expr) => expr.type === `agg`\n )\n if (hasAggregates) {\n // Handle implicit single-group aggregation\n pipeline = processGroupBy(\n pipeline,\n [], // Empty group by means single group\n query.having,\n query.select,\n query.fnHaving\n )\n }\n }\n\n // Process the HAVING clause if it exists (only applies after GROUP BY)\n if (query.having && (!query.groupBy || query.groupBy.length === 0)) {\n // Check if we have aggregates in SELECT that would trigger implicit grouping\n const hasAggregates = query.select\n ? Object.values(query.select).some((expr) => expr.type === `agg`)\n : false\n\n if (!hasAggregates) {\n throw new Error(`HAVING clause requires GROUP BY clause`)\n }\n }\n\n // Process functional HAVING clauses outside of GROUP BY (treat as additional WHERE filters)\n if (\n query.fnHaving &&\n query.fnHaving.length > 0 &&\n (!query.groupBy || query.groupBy.length === 0)\n ) {\n // If there's no GROUP BY but there are fnHaving clauses, apply them as filters\n for (const fnHaving of query.fnHaving) {\n pipeline = pipeline.pipe(\n filter(([_key, namespacedRow]) => {\n return fnHaving(namespacedRow)\n })\n )\n }\n }\n\n // Process the DISTINCT clause if it exists\n if (query.distinct) {\n pipeline = pipeline.pipe(distinct(([_key, row]) => row.__select_results))\n }\n\n // Process orderBy parameter if it exists\n if (query.orderBy && query.orderBy.length > 0) {\n const orderedPipeline = processOrderBy(\n pipeline,\n query.orderBy,\n query.limit,\n query.offset\n )\n\n // Final step: extract the __select_results and include orderBy index\n const resultPipeline = orderedPipeline.pipe(\n map(([key, [row, orderByIndex]]) => {\n // Extract the final results from __select_results and include orderBy index\n const finalResults = (row as any).__select_results\n return [key, [finalResults, orderByIndex]] as [unknown, [any, string]]\n })\n )\n\n const result = resultPipeline\n // Cache the result before returning\n cache.set(query, result)\n return result\n } else if (query.limit !== undefined || query.offset !== undefined) {\n // If there's a limit or offset without orderBy, throw an error\n throw new Error(\n `LIMIT and OFFSET require an ORDER BY clause to ensure deterministic results`\n )\n }\n\n // Final step: extract the __select_results and return tuple format (no orderBy)\n const resultPipeline: ResultStream = pipeline.pipe(\n map(([key, row]) => {\n // Extract the final results from __select_results and return [key, [results, undefined]]\n const finalResults = (row as any).__select_results\n return [key, [finalResults, undefined]] as [\n unknown,\n [any, string | undefined],\n ]\n })\n )\n\n const result = resultPipeline\n // Cache the result before returning\n cache.set(query, result)\n return result\n}\n\n/**\n * Processes the FROM clause to extract the main table alias and input stream\n */\nfunction processFrom(\n from: CollectionRef | QueryRef,\n allInputs: Record<string, KeyedStream>,\n cache: QueryCache\n): { alias: string; input: KeyedStream } {\n switch (from.type) {\n case `collectionRef`: {\n const input = allInputs[from.collection.id]\n if (!input) {\n throw new Error(\n `Input for collection \"${from.collection.id}\" not found in inputs map`\n )\n }\n return { alias: from.alias, input }\n }\n case `queryRef`: {\n // Recursively compile the sub-query with cache\n const subQueryInput = compileQuery(from.query, allInputs, cache)\n\n // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)\n // We need to extract just the value for use in parent queries\n const extractedInput = subQueryInput.pipe(\n map((data: any) => {\n const [key, [value, _orderByIndex]] = data\n return [key, value] as [unknown, any]\n })\n )\n\n return { alias: from.alias, input: extractedInput }\n }\n default:\n throw new Error(`Unsupported FROM type: ${(from as any).type}`)\n }\n}\n"],"names":["resultPipeline","result"],"mappings":"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"}
@@ -10,6 +10,7 @@ export interface QueryIR {
10
10
  orderBy?: OrderBy;
11
11
  limit?: Limit;
12
12
  offset?: Offset;
13
+ distinct?: true;
13
14
  fnSelect?: (row: NamespacedRow) => any;
14
15
  fnWhere?: Array<(row: NamespacedRow) => any>;
15
16
  fnHaving?: Array<(row: NamespacedRow) => any>;
@@ -1 +1 @@
1
- {"version":3,"file":"ir.js","sources":["../../../src/query/ir.ts"],"sourcesContent":["/*\nThis is the intermediate representation of the query.\n*/\n\nimport type { CollectionImpl } from \"../collection\"\nimport type { NamespacedRow } from \"../types\"\n\nexport interface QueryIR {\n from: From\n select?: Select\n join?: Join\n where?: Array<Where>\n groupBy?: GroupBy\n having?: Array<Having>\n orderBy?: OrderBy\n limit?: Limit\n offset?: Offset\n\n // Functional variants\n fnSelect?: (row: NamespacedRow) => any\n fnWhere?: Array<(row: NamespacedRow) => any>\n fnHaving?: Array<(row: NamespacedRow) => any>\n}\n\nexport type From = CollectionRef | QueryRef\n\nexport type Select = {\n [alias: string]: BasicExpression | Aggregate\n}\n\nexport type Join = Array<JoinClause>\n\nexport interface JoinClause {\n from: CollectionRef | QueryRef\n type: `left` | `right` | `inner` | `outer` | `full` | `cross`\n left: BasicExpression\n right: BasicExpression\n}\n\nexport type Where = BasicExpression<boolean>\n\nexport type GroupBy = Array<BasicExpression>\n\nexport type Having = Where\n\nexport type OrderBy = Array<OrderByClause>\n\nexport type OrderByClause = {\n expression: BasicExpression\n direction: OrderByDirection\n}\n\nexport type OrderByDirection = `asc` | `desc`\n\nexport type Limit = number\n\nexport type Offset = number\n\n/* Expressions */\n\nabstract class BaseExpression<T = any> {\n public abstract type: string\n /** @internal - Type brand for TypeScript inference */\n declare readonly __returnType: T\n}\n\nexport class CollectionRef extends BaseExpression {\n public type = `collectionRef` as const\n constructor(\n public collection: CollectionImpl,\n public alias: string\n ) {\n super()\n }\n}\n\nexport class QueryRef extends BaseExpression {\n public type = `queryRef` as const\n constructor(\n public query: QueryIR,\n public alias: string\n ) {\n super()\n }\n}\n\nexport class PropRef<T = any> extends BaseExpression<T> {\n public type = `ref` as const\n constructor(\n public path: Array<string> // path to the property in the collection, with the alias as the first element\n ) {\n super()\n }\n}\n\nexport class Value<T = any> extends BaseExpression<T> {\n public type = `val` as const\n constructor(\n public value: T // any js value\n ) {\n super()\n }\n}\n\nexport class Func<T = any> extends BaseExpression<T> {\n public type = `func` as const\n constructor(\n public name: string, // such as eq, gt, lt, upper, lower, etc.\n public args: Array<BasicExpression>\n ) {\n super()\n }\n}\n\n// This is the basic expression type that is used in the majority of expression\n// builder callbacks (select, where, groupBy, having, orderBy, etc.)\n// it doesn't include aggregate functions as those are only used in the select clause\nexport type BasicExpression<T = any> = PropRef<T> | Value<T> | Func<T>\n\nexport class Aggregate<T = any> extends BaseExpression<T> {\n public type = `agg` as const\n constructor(\n public name: string, // such as count, avg, sum, min, max, etc.\n public args: Array<BasicExpression>\n ) {\n super()\n }\n}\n"],"names":[],"mappings":"AA4DA,MAAe,eAAwB;AAIvC;AAEO,MAAM,sBAAsB,eAAe;AAAA,EAEhD,YACS,YACA,OACP;AACA,UAAA;AAHO,SAAA,aAAA;AACA,SAAA,QAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAEO,MAAM,iBAAiB,eAAe;AAAA,EAE3C,YACS,OACA,OACP;AACA,UAAA;AAHO,SAAA,QAAA;AACA,SAAA,QAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAEO,MAAM,gBAAyB,eAAkB;AAAA,EAEtD,YACS,MACP;AACA,UAAA;AAFO,SAAA,OAAA;AAFT,SAAO,OAAO;AAAA,EAKd;AACF;AAEO,MAAM,cAAuB,eAAkB;AAAA,EAEpD,YACS,OACP;AACA,UAAA;AAFO,SAAA,QAAA;AAFT,SAAO,OAAO;AAAA,EAKd;AACF;AAEO,MAAM,aAAsB,eAAkB;AAAA,EAEnD,YACS,MACA,MACP;AACA,UAAA;AAHO,SAAA,OAAA;AACA,SAAA,OAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAOO,MAAM,kBAA2B,eAAkB;AAAA,EAExD,YACS,MACA,MACP;AACA,UAAA;AAHO,SAAA,OAAA;AACA,SAAA,OAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;"}
1
+ {"version":3,"file":"ir.js","sources":["../../../src/query/ir.ts"],"sourcesContent":["/*\nThis is the intermediate representation of the query.\n*/\n\nimport type { CollectionImpl } from \"../collection\"\nimport type { NamespacedRow } from \"../types\"\n\nexport interface QueryIR {\n from: From\n select?: Select\n join?: Join\n where?: Array<Where>\n groupBy?: GroupBy\n having?: Array<Having>\n orderBy?: OrderBy\n limit?: Limit\n offset?: Offset\n distinct?: true\n\n // Functional variants\n fnSelect?: (row: NamespacedRow) => any\n fnWhere?: Array<(row: NamespacedRow) => any>\n fnHaving?: Array<(row: NamespacedRow) => any>\n}\n\nexport type From = CollectionRef | QueryRef\n\nexport type Select = {\n [alias: string]: BasicExpression | Aggregate\n}\n\nexport type Join = Array<JoinClause>\n\nexport interface JoinClause {\n from: CollectionRef | QueryRef\n type: `left` | `right` | `inner` | `outer` | `full` | `cross`\n left: BasicExpression\n right: BasicExpression\n}\n\nexport type Where = BasicExpression<boolean>\n\nexport type GroupBy = Array<BasicExpression>\n\nexport type Having = Where\n\nexport type OrderBy = Array<OrderByClause>\n\nexport type OrderByClause = {\n expression: BasicExpression\n direction: OrderByDirection\n}\n\nexport type OrderByDirection = `asc` | `desc`\n\nexport type Limit = number\n\nexport type Offset = number\n\n/* Expressions */\n\nabstract class BaseExpression<T = any> {\n public abstract type: string\n /** @internal - Type brand for TypeScript inference */\n declare readonly __returnType: T\n}\n\nexport class CollectionRef extends BaseExpression {\n public type = `collectionRef` as const\n constructor(\n public collection: CollectionImpl,\n public alias: string\n ) {\n super()\n }\n}\n\nexport class QueryRef extends BaseExpression {\n public type = `queryRef` as const\n constructor(\n public query: QueryIR,\n public alias: string\n ) {\n super()\n }\n}\n\nexport class PropRef<T = any> extends BaseExpression<T> {\n public type = `ref` as const\n constructor(\n public path: Array<string> // path to the property in the collection, with the alias as the first element\n ) {\n super()\n }\n}\n\nexport class Value<T = any> extends BaseExpression<T> {\n public type = `val` as const\n constructor(\n public value: T // any js value\n ) {\n super()\n }\n}\n\nexport class Func<T = any> extends BaseExpression<T> {\n public type = `func` as const\n constructor(\n public name: string, // such as eq, gt, lt, upper, lower, etc.\n public args: Array<BasicExpression>\n ) {\n super()\n }\n}\n\n// This is the basic expression type that is used in the majority of expression\n// builder callbacks (select, where, groupBy, having, orderBy, etc.)\n// it doesn't include aggregate functions as those are only used in the select clause\nexport type BasicExpression<T = any> = PropRef<T> | Value<T> | Func<T>\n\nexport class Aggregate<T = any> extends BaseExpression<T> {\n public type = `agg` as const\n constructor(\n public name: string, // such as count, avg, sum, min, max, etc.\n public args: Array<BasicExpression>\n ) {\n super()\n }\n}\n"],"names":[],"mappings":"AA6DA,MAAe,eAAwB;AAIvC;AAEO,MAAM,sBAAsB,eAAe;AAAA,EAEhD,YACS,YACA,OACP;AACA,UAAA;AAHO,SAAA,aAAA;AACA,SAAA,QAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAEO,MAAM,iBAAiB,eAAe;AAAA,EAE3C,YACS,OACA,OACP;AACA,UAAA;AAHO,SAAA,QAAA;AACA,SAAA,QAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAEO,MAAM,gBAAyB,eAAkB;AAAA,EAEtD,YACS,MACP;AACA,UAAA;AAFO,SAAA,OAAA;AAFT,SAAO,OAAO;AAAA,EAKd;AACF;AAEO,MAAM,cAAuB,eAAkB;AAAA,EAEpD,YACS,OACP;AACA,UAAA;AAFO,SAAA,QAAA;AAFT,SAAO,OAAO;AAAA,EAKd;AACF;AAEO,MAAM,aAAsB,eAAkB;AAAA,EAEnD,YACS,MACA,MACP;AACA,UAAA;AAHO,SAAA,OAAA;AACA,SAAA,OAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;AAOO,MAAM,kBAA2B,eAAkB;AAAA,EAExD,YACS,MACA,MACP;AACA,UAAA;AAHO,SAAA,OAAA;AACA,SAAA,OAAA;AAHT,SAAO,OAAO;AAAA,EAMd;AACF;"}
@@ -1,5 +1,5 @@
1
1
  import { Deferred } from './deferred.js';
2
- import { MutationFn, OperationType, PendingMutation, TransactionConfig, TransactionState } from './types.js';
2
+ import { MutationFn, PendingMutation, TransactionConfig, TransactionState } from './types.js';
3
3
  /**
4
4
  * Creates a new transaction for grouping multiple collection operations
5
5
  * @param config - Transaction configuration with mutation function
@@ -52,7 +52,7 @@ import { MutationFn, OperationType, PendingMutation, TransactionConfig, Transact
52
52
  * // Commit later
53
53
  * await tx.commit()
54
54
  */
55
- export declare function createTransaction<TData extends object = Record<string, unknown>>(config: TransactionConfig<TData>): Transaction<TData>;
55
+ export declare function createTransaction<T extends object = Record<string, unknown>>(config: TransactionConfig<T>): Transaction<T>;
56
56
  /**
57
57
  * Gets the currently active ambient transaction, if any
58
58
  * Used internally by collection operations to join existing transactions
@@ -65,12 +65,12 @@ export declare function createTransaction<TData extends object = Record<string,
65
65
  * }
66
66
  */
67
67
  export declare function getActiveTransaction(): Transaction | undefined;
68
- declare class Transaction<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType> {
68
+ declare class Transaction<T extends object = Record<string, unknown>> {
69
69
  id: string;
70
70
  state: TransactionState;
71
71
  mutationFn: MutationFn<T>;
72
- mutations: Array<PendingMutation<T, TOperation>>;
73
- isPersisted: Deferred<Transaction<T, TOperation>>;
72
+ mutations: Array<PendingMutation<T>>;
73
+ isPersisted: Deferred<Transaction<T>>;
74
74
  autoCommit: boolean;
75
75
  createdAt: Date;
76
76
  sequenceNumber: number;
@@ -1 +1 @@
1
- {"version":3,"file":"transactions.js","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n MutationFn,\n OperationType,\n PendingMutation,\n TransactionConfig,\n TransactionState,\n TransactionWithMutations,\n} from \"./types\"\n\nconst transactions: Array<Transaction<any>> = []\nlet transactionStack: Array<Transaction<any>> = []\n\nlet sequenceNumber = 0\n\n/**\n * Creates a new transaction for grouping multiple collection operations\n * @param config - Transaction configuration with mutation function\n * @returns A new Transaction instance\n * @example\n * // Basic transaction usage\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Send all mutations to API\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle transaction errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"New item\" })\n * })\n *\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async () => {\n * // API call\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later\n * await tx.commit()\n */\nexport function createTransaction<\n TData extends object = Record<string, unknown>,\n>(config: TransactionConfig<TData>): Transaction<TData> {\n const newTransaction = new Transaction<TData>(config)\n transactions.push(newTransaction)\n return newTransaction\n}\n\n/**\n * Gets the currently active ambient transaction, if any\n * Used internally by collection operations to join existing transactions\n * @returns The active transaction or undefined if none is active\n * @example\n * // Check if operations will join an ambient transaction\n * const ambientTx = getActiveTransaction()\n * if (ambientTx) {\n * console.log('Operations will join transaction:', ambientTx.id)\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<any>) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction<any>) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction<any>) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nclass Transaction<\n T extends object = Record<string, unknown>,\n TOperation extends OperationType = OperationType,\n> {\n public id: string\n public state: TransactionState\n public mutationFn: MutationFn<T>\n public mutations: Array<PendingMutation<T, TOperation>>\n public isPersisted: Deferred<Transaction<T, TOperation>>\n public autoCommit: boolean\n public createdAt: Date\n public sequenceNumber: number\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig<T>) {\n if (typeof config.mutationFn === `undefined`) {\n throw `mutationFn is required when creating a transaction`\n }\n this.id = config.id ?? crypto.randomUUID()\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred<Transaction<T, TOperation>>()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.sequenceNumber = sequenceNumber++\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 /**\n * Execute collection operations within this transaction\n * @param callback - Function containing collection operations to group together\n * @returns This transaction for chaining\n * @example\n * // Group multiple operations\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * collection.delete(\"3\")\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle mutate errors\n * try {\n * tx.mutate(() => {\n * collection.insert({ id: \"invalid\" }) // This might throw\n * })\n * } catch (error) {\n * console.log('Mutation failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({ autoCommit: false, mutationFn: async () => {} })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later when ready\n * await tx.commit()\n */\n mutate(callback: () => void): Transaction<T> {\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.globalKey === newMutation.globalKey\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 /**\n * Rollback the transaction and any conflicting transactions\n * @param config - Configuration for rollback behavior\n * @returns This transaction for chaining\n * @example\n * // Manual rollback\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * // Rollback if needed\n * if (shouldCancel) {\n * tx.rollback()\n * }\n *\n * @example\n * // Handle rollback cascade (automatic)\n * const tx1 = createTransaction({ mutationFn: async () => {} })\n * const tx2 = createTransaction({ mutationFn: async () => {} })\n *\n * tx1.mutate(() => collection.update(\"1\", draft => { draft.value = \"A\" }))\n * tx2.mutate(() => collection.update(\"1\", draft => { draft.value = \"B\" })) // Same item\n *\n * tx1.rollback() // This will also rollback tx2 due to conflict\n *\n * @example\n * // Handle rollback in error scenarios\n * try {\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction was rolled back:', error)\n * // Transaction automatically rolled back on mutation function failure\n * }\n */\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {\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.globalKey))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.globalKey)) &&\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.onTransactionStateChange()\n\n // Only call commitPendingTransactions if there are pending sync transactions\n if (mutation.collection.pendingSyncedTransactions.length > 0) {\n mutation.collection.commitPendingTransactions()\n }\n\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n /**\n * Commit the transaction and execute the mutation function\n * @returns Promise that resolves to this transaction when complete\n * @example\n * // Manual commit (when autoCommit is false)\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * await tx.commit() // Manually commit\n *\n * @example\n * // Handle commit errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * await tx.commit()\n * } catch (error) {\n * console.log('Commit failed, transaction rolled back:', error)\n * }\n *\n * @example\n * // Check transaction state after commit\n * await tx.commit()\n * console.log(tx.state) // \"completed\" or \"failed\"\n */\n async commit(): Promise<Transaction<T>> {\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 // We've already verified mutations is non-empty, so this cast is safe\n // Use a direct type assertion instead of object spreading to preserve the original type\n await this.mutationFn({\n transaction: this as unknown as TransactionWithMutations<T>,\n })\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 /**\n * Compare two transactions by their createdAt time and sequence number in order\n * to sort them in the order they were created.\n * @param other - The other transaction to compare to\n * @returns -1 if this transaction was created before the other, 1 if it was created after, 0 if they were created at the same time\n */\n compareCreatedAt(other: Transaction<any>): number {\n const createdAtComparison =\n this.createdAt.getTime() - other.createdAt.getTime()\n if (createdAtComparison !== 0) {\n return createdAtComparison\n }\n return this.sequenceNumber - other.sequenceNumber\n }\n}\n\nexport type { Transaction }\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transactions.js","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n MutationFn,\n PendingMutation,\n TransactionConfig,\n TransactionState,\n TransactionWithMutations,\n} from \"./types\"\n\nconst transactions: Array<Transaction<any>> = []\nlet transactionStack: Array<Transaction<any>> = []\n\nlet sequenceNumber = 0\n\n/**\n * Creates a new transaction for grouping multiple collection operations\n * @param config - Transaction configuration with mutation function\n * @returns A new Transaction instance\n * @example\n * // Basic transaction usage\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Send all mutations to API\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle transaction errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"New item\" })\n * })\n *\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async () => {\n * // API call\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later\n * await tx.commit()\n */\nexport function createTransaction<T extends object = Record<string, unknown>>(\n config: TransactionConfig<T>\n): Transaction<T> {\n const newTransaction = new Transaction<T>(config)\n transactions.push(newTransaction)\n return newTransaction\n}\n\n/**\n * Gets the currently active ambient transaction, if any\n * Used internally by collection operations to join existing transactions\n * @returns The active transaction or undefined if none is active\n * @example\n * // Check if operations will join an ambient transaction\n * const ambientTx = getActiveTransaction()\n * if (ambientTx) {\n * console.log('Operations will join transaction:', ambientTx.id)\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<any>) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction<any>) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction<any>) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nclass Transaction<T extends object = Record<string, unknown>> {\n public id: string\n public state: TransactionState\n public mutationFn: MutationFn<T>\n public mutations: Array<PendingMutation<T>>\n public isPersisted: Deferred<Transaction<T>>\n public autoCommit: boolean\n public createdAt: Date\n public sequenceNumber: number\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig<T>) {\n if (typeof config.mutationFn === `undefined`) {\n throw `mutationFn is required when creating a transaction`\n }\n this.id = config.id ?? crypto.randomUUID()\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred<Transaction<T>>()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.sequenceNumber = sequenceNumber++\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 /**\n * Execute collection operations within this transaction\n * @param callback - Function containing collection operations to group together\n * @returns This transaction for chaining\n * @example\n * // Group multiple operations\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * collection.delete(\"3\")\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle mutate errors\n * try {\n * tx.mutate(() => {\n * collection.insert({ id: \"invalid\" }) // This might throw\n * })\n * } catch (error) {\n * console.log('Mutation failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({ autoCommit: false, mutationFn: async () => {} })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later when ready\n * await tx.commit()\n */\n mutate(callback: () => void): Transaction<T> {\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.globalKey === newMutation.globalKey\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 /**\n * Rollback the transaction and any conflicting transactions\n * @param config - Configuration for rollback behavior\n * @returns This transaction for chaining\n * @example\n * // Manual rollback\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * // Rollback if needed\n * if (shouldCancel) {\n * tx.rollback()\n * }\n *\n * @example\n * // Handle rollback cascade (automatic)\n * const tx1 = createTransaction({ mutationFn: async () => {} })\n * const tx2 = createTransaction({ mutationFn: async () => {} })\n *\n * tx1.mutate(() => collection.update(\"1\", draft => { draft.value = \"A\" }))\n * tx2.mutate(() => collection.update(\"1\", draft => { draft.value = \"B\" })) // Same item\n *\n * tx1.rollback() // This will also rollback tx2 due to conflict\n *\n * @example\n * // Handle rollback in error scenarios\n * try {\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction was rolled back:', error)\n * // Transaction automatically rolled back on mutation function failure\n * }\n */\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {\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.globalKey))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.globalKey)) &&\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.onTransactionStateChange()\n\n // Only call commitPendingTransactions if there are pending sync transactions\n if (mutation.collection.pendingSyncedTransactions.length > 0) {\n mutation.collection.commitPendingTransactions()\n }\n\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n /**\n * Commit the transaction and execute the mutation function\n * @returns Promise that resolves to this transaction when complete\n * @example\n * // Manual commit (when autoCommit is false)\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * await tx.commit() // Manually commit\n *\n * @example\n * // Handle commit errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * await tx.commit()\n * } catch (error) {\n * console.log('Commit failed, transaction rolled back:', error)\n * }\n *\n * @example\n * // Check transaction state after commit\n * await tx.commit()\n * console.log(tx.state) // \"completed\" or \"failed\"\n */\n async commit(): Promise<Transaction<T>> {\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 // We've already verified mutations is non-empty, so this cast is safe\n // Use a direct type assertion instead of object spreading to preserve the original type\n await this.mutationFn({\n transaction: this as unknown as TransactionWithMutations<T>,\n })\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 /**\n * Compare two transactions by their createdAt time and sequence number in order\n * to sort them in the order they were created.\n * @param other - The other transaction to compare to\n * @returns -1 if this transaction was created before the other, 1 if it was created after, 0 if they were created at the same time\n */\n compareCreatedAt(other: Transaction<any>): number {\n const createdAtComparison =\n this.createdAt.getTime() - other.createdAt.getTime()\n if (createdAtComparison !== 0) {\n return createdAtComparison\n }\n return this.sequenceNumber - other.sequenceNumber\n }\n}\n\nexport type { Transaction }\n"],"names":[],"mappings":"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"}
@@ -8,6 +8,24 @@ import { Transaction } from './transactions.js';
8
8
  * @internal This is used by the type resolution system
9
9
  */
10
10
  export type InferSchemaOutput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<T> extends object ? StandardSchemaV1.InferOutput<T> : Record<string, unknown> : Record<string, unknown>;
11
+ /**
12
+ * Helper type to extract the input type from a standard schema
13
+ *
14
+ * @internal This is used for collection insert type inference
15
+ */
16
+ export type InferSchemaInput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferInput<T> extends object ? StandardSchemaV1.InferInput<T> : Record<string, unknown> : Record<string, unknown>;
17
+ /**
18
+ * Helper type to determine the insert input type
19
+ * This takes the raw generics (TExplicit, TSchema, TFallback) instead of the resolved T.
20
+ *
21
+ * Priority:
22
+ * 1. Explicit generic TExplicit (if not 'unknown')
23
+ * 2. Schema input type (if schema provided)
24
+ * 3. Fallback type TFallback
25
+ *
26
+ * @internal This is used for collection insert type inference
27
+ */
28
+ export type ResolveInsertInput<TExplicit = unknown, TSchema extends StandardSchemaV1 = never, TFallback extends object = Record<string, unknown>> = unknown extends TExplicit ? [TSchema] extends [never] ? TFallback : InferSchemaInput<TSchema> : TExplicit extends object ? TExplicit : Record<string, unknown>;
11
29
  /**
12
30
  * Helper type to determine the final type based on priority:
13
31
  * 1. Explicit generic TExplicit (if not 'unknown')
@@ -28,25 +46,32 @@ export type Fn = (...args: Array<any>) => any;
28
46
  * A record of utility functions that can be attached to a collection
29
47
  */
30
48
  export type UtilsRecord = Record<string, Fn>;
49
+ /**
50
+ *
51
+ * @remarks `update` and `insert` are both represented as `Partial<T>`, but changes for `insert` could me made more precise by inferring the schema input type. In practice, this has almost 0 real world impact so it's not worth the added type complexity.
52
+ *
53
+ * @see https://github.com/TanStack/db/pull/209#issuecomment-3053001206
54
+ */
55
+ export type ResolveTransactionChanges<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType> = TOperation extends `delete` ? T : Partial<T>;
31
56
  /**
32
57
  * Represents a pending mutation within a transaction
33
58
  * Contains information about the original and modified data, as well as metadata
34
59
  */
35
- export interface PendingMutation<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType> {
60
+ export interface PendingMutation<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType, TCollection extends Collection<T, any, any, any, any> = Collection<T, any, any, any, any>> {
36
61
  mutationId: string;
37
62
  original: TOperation extends `insert` ? {} : T;
38
63
  modified: T;
39
- changes: TOperation extends `insert` ? T : TOperation extends `delete` ? T : Partial<T>;
64
+ changes: ResolveTransactionChanges<T, TOperation>;
40
65
  globalKey: string;
41
66
  key: any;
42
- type: OperationType;
67
+ type: TOperation;
43
68
  metadata: unknown;
44
69
  syncMetadata: Record<string, unknown>;
45
70
  /** Whether this mutation should be applied optimistically (defaults to true) */
46
71
  optimistic: boolean;
47
72
  createdAt: Date;
48
73
  updatedAt: Date;
49
- collection: Collection<T, any, any>;
74
+ collection: TCollection;
50
75
  }
51
76
  /**
52
77
  * Configuration options for creating a new transaction
@@ -63,7 +88,7 @@ export type NonEmptyArray<T> = [T, ...Array<T>];
63
88
  * Utility type for a Transaction with at least one mutation
64
89
  * This is used internally by the Transaction.commit method
65
90
  */
66
- export type TransactionWithMutations<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType> = Transaction<T, TOperation> & {
91
+ export type TransactionWithMutations<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType> = Transaction<T> & {
67
92
  mutations: NonEmptyArray<PendingMutation<T, TOperation>>;
68
93
  };
69
94
  export interface TransactionConfig<T extends object = Record<string, unknown>> {
@@ -77,11 +102,11 @@ export interface TransactionConfig<T extends object = Record<string, unknown>> {
77
102
  /**
78
103
  * Options for the createOptimisticAction helper
79
104
  */
80
- export interface CreateOptimisticActionsOptions<TVars = unknown> extends Omit<TransactionConfig, `mutationFn`> {
105
+ export interface CreateOptimisticActionsOptions<TVars = unknown, T extends object = Record<string, unknown>> extends Omit<TransactionConfig<T>, `mutationFn`> {
81
106
  /** Function to apply optimistic updates locally before the mutation completes */
82
107
  onMutate: (vars: TVars) => void;
83
108
  /** Function to execute the mutation on the server */
84
- mutationFn: (vars: TVars, params: MutationFnParams) => Promise<any>;
109
+ mutationFn: (vars: TVars, params: MutationFnParams<T>) => Promise<any>;
85
110
  }
86
111
  export type { Transaction };
87
112
  type Value<TExtensions = never> = string | number | boolean | bigint | null | TExtensions | Array<Value<TExtensions>> | {
@@ -91,7 +116,7 @@ export type Row<TExtensions = never> = Record<string, Value<TExtensions>>;
91
116
  export type OperationType = `insert` | `update` | `delete`;
92
117
  export interface SyncConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number> {
93
118
  sync: (params: {
94
- collection: Collection<T, TKey>;
119
+ collection: Collection<T, TKey, any, any, any>;
95
120
  begin: () => void;
96
121
  write: (message: Omit<ChangeMessage<T>, `key`>) => void;
97
122
  commit: () => void;
@@ -189,7 +214,7 @@ export type CollectionStatus =
189
214
  | `error`
190
215
  /** Collection has been cleaned up and resources freed */
191
216
  | `cleaned-up`;
192
- export interface CollectionConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = StandardSchemaV1> {
217
+ export interface CollectionConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = StandardSchemaV1, TInsertInput extends object = T> {
193
218
  id?: string;
194
219
  sync: SyncConfig<T, TKey>;
195
220
  schema?: TSchema;
@@ -265,7 +290,7 @@ export interface CollectionConfig<T extends object = Record<string, unknown>, TK
265
290
  * })
266
291
  * }
267
292
  */
268
- onInsert?: InsertMutationFn<T, TKey>;
293
+ onInsert?: InsertMutationFn<TInsertInput, TKey>;
269
294
  /**
270
295
  * Optional asynchronous handler function called before an update operation
271
296
  * @param params Object containing transaction and collection information
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@tanstack/db",
3
3
  "description": "A reactive client store for building super fast apps on sync",
4
- "version": "0.0.21",
4
+ "version": "0.0.23",
5
5
  "dependencies": {
6
- "@electric-sql/d2mini": "^0.1.6",
6
+ "@electric-sql/d2mini": "^0.1.7",
7
7
  "@standard-schema/spec": "^1.0.0"
8
8
  },
9
9
  "devDependencies": {