@shotleybuilder/svelte-gridlite-kit 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -110,7 +110,7 @@ Focused, single-feature demo pages in `src/routes/examples/`:
110
110
  | `/examples/filtering` | FilterBar + programmatic filter buttons |
111
111
  | `/examples/grouping` | Hierarchical grouping with aggregations |
112
112
  | `/examples/custom-cells` | Currency, date, boolean, rating star formatters |
113
- | `/examples/raw-query` | JOIN, aggregate, CTE queries via the `query` prop |
113
+ | `/examples/raw-query` | JOIN, aggregate, CTE queries via `query` prop with full toolbar |
114
114
 
115
115
  Run `npm run dev` and visit `http://localhost:5173/examples/minimal` to start.
116
116
 
@@ -164,33 +164,31 @@ async function rebuildQuery() {
164
164
  }
165
165
  let sql;
166
166
  let params = [];
167
- if (query) {
168
- sql = query;
169
- } else if (table) {
170
- if (isGrouped) {
171
- await rebuildGroupedQuery();
172
- return;
173
- }
174
- const usePagination = features.pagination !== false;
175
- const built = buildQuery({
176
- table,
177
- filters,
178
- filterLogic,
179
- sorting,
180
- page: usePagination ? page : void 0,
181
- pageSize: usePagination ? pageSize : void 0,
182
- allowedColumns,
183
- globalSearch: globalFilter || void 0
184
- });
185
- sql = built.sql;
186
- params = built.params;
187
- if (usePagination) {
188
- await updateTotalCount();
189
- }
190
- } else {
167
+ const querySource = query ? { source: query } : table ? { table } : null;
168
+ if (!querySource) {
191
169
  error = "Either `table` or `query` prop is required";
192
170
  return;
193
171
  }
172
+ if (isGrouped) {
173
+ await rebuildGroupedQuery();
174
+ return;
175
+ }
176
+ const usePagination = features.pagination !== false;
177
+ const built = buildQuery({
178
+ ...querySource,
179
+ filters,
180
+ filterLogic,
181
+ sorting,
182
+ page: usePagination ? page : void 0,
183
+ pageSize: usePagination ? pageSize : void 0,
184
+ allowedColumns,
185
+ globalSearch: globalFilter || void 0
186
+ });
187
+ sql = built.sql;
188
+ params = built.params;
189
+ if (usePagination) {
190
+ await updateTotalCount();
191
+ }
194
192
  groupData = [];
195
193
  expandedGroups = /* @__PURE__ */ new Set();
196
194
  totalGroups = 0;
@@ -216,12 +214,13 @@ function cleanAgg(g) {
216
214
  };
217
215
  }
218
216
  async function rebuildGroupedQuery() {
219
- if (!table) return;
217
+ const querySource = query ? { source: query } : table ? { table } : null;
218
+ if (!querySource) return;
220
219
  try {
221
220
  const usePagination = features.pagination !== false;
222
221
  const topGroupConfig = cleanAgg(validGrouping[0]);
223
222
  const summaryQuery = buildGroupSummaryQuery({
224
- table,
223
+ ...querySource,
225
224
  grouping: [topGroupConfig],
226
225
  filters,
227
226
  filterLogic,
@@ -237,7 +236,7 @@ async function rebuildGroupedQuery() {
237
236
  );
238
237
  if (usePagination) {
239
238
  const countQuery = buildGroupCountQuery({
240
- table,
239
+ ...querySource,
241
240
  grouping: [topGroupConfig],
242
241
  filters,
243
242
  filterLogic,
@@ -283,7 +282,8 @@ async function rebuildGroupedQuery() {
283
282
  }
284
283
  }
285
284
  async function fetchGroupChildren(group) {
286
- if (!table) return;
285
+ const querySource = query ? { source: query } : table ? { table } : null;
286
+ if (!querySource) return;
287
287
  const key = groupKey(group);
288
288
  groupLoading = /* @__PURE__ */ new Set([...groupLoading, key]);
289
289
  try {
@@ -295,7 +295,7 @@ async function fetchGroupChildren(group) {
295
295
  if (nextDepth < validGrouping.length) {
296
296
  const subGroupConfig = cleanAgg(validGrouping[nextDepth]);
297
297
  const summaryQuery = buildGroupSummaryQuery({
298
- table,
298
+ ...querySource,
299
299
  grouping: [subGroupConfig],
300
300
  filters: [
301
301
  ...filters,
@@ -334,7 +334,7 @@ async function fetchGroupChildren(group) {
334
334
  updateGroupInTree(key, { subGroups });
335
335
  } else {
336
336
  const detailQuery = buildGroupDetailQuery({
337
- table,
337
+ ...querySource,
338
338
  groupValues: parentValues,
339
339
  filters,
340
340
  filterLogic,
@@ -372,10 +372,11 @@ function updateGroupNode(node, targetKey, updates) {
372
372
  return node;
373
373
  }
374
374
  async function updateTotalCount() {
375
- if (!table) return;
375
+ const querySource = query ? { source: query } : table ? { table } : null;
376
+ if (!querySource) return;
376
377
  try {
377
378
  const countQuery = buildCountQuery({
378
- table,
379
+ ...querySource,
379
380
  filters,
380
381
  filterLogic,
381
382
  allowedColumns,
@@ -804,7 +805,7 @@ onDestroy(() => {
804
805
  {:else if storeState.error}
805
806
  <div class="gridlite-empty">Error: {storeState.error.message}</div>
806
807
  {:else}
807
- {#if table && toolbarLayout !== 'aggrid'}
808
+ {#if toolbarLayout !== 'aggrid'}
808
809
  <div class="gridlite-toolbar">
809
810
  <!-- Custom toolbar content (start) -->
810
811
  <slot name="toolbar-start" />
@@ -847,7 +848,8 @@ onDestroy(() => {
847
848
  <div class="gridlite-toolbar-filter">
848
849
  <FilterBar
849
850
  {db}
850
- {table}
851
+ table={table ?? ''}
852
+ source={query ?? ''}
851
853
  {columns}
852
854
  columnConfigs={mergedColumnConfigs}
853
855
  {allowedColumns}
@@ -993,7 +995,7 @@ onDestroy(() => {
993
995
  </div>
994
996
  {/if}
995
997
 
996
- {#if table && toolbarLayout === 'aggrid'}
998
+ {#if toolbarLayout === 'aggrid'}
997
999
  <!-- AG Grid layout: sidebar on right, minimal toolbar on top -->
998
1000
  <!-- TODO(#1): aggrid layout is experimental — not production-ready. Needs debugging. -->
999
1001
  <div class="gridlite-toolbar gridlite-toolbar-aggrid-top">
@@ -1159,32 +1161,30 @@ onDestroy(() => {
1159
1161
  {getColumnLabel(col)}
1160
1162
  </span>
1161
1163
  {/if}
1162
- {#if table}
1163
- <button
1164
- class="gridlite-th-menu-btn"
1165
- on:click|stopPropagation={() =>
1166
- (columnMenuOpen = columnMenuOpen === col.name ? null : col.name)}
1167
- title="Column options"
1168
- type="button"
1169
- >
1170
- <svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1171
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
1172
- </svg>
1173
- </button>
1174
- <ColumnMenu
1175
- columnName={col.name}
1176
- isOpen={columnMenuOpen === col.name}
1177
- {sorting}
1178
- canSort={features.sorting ?? false}
1179
- canFilter={features.filtering ?? false}
1180
- canGroup={features.grouping ?? false}
1181
- onSort={handleColumnMenuSort}
1182
- onFilter={handleColumnMenuFilter}
1183
- onGroup={handleColumnMenuGroup}
1184
- onHide={handleColumnMenuHide}
1185
- onClose={() => (columnMenuOpen = null)}
1186
- />
1187
- {/if}
1164
+ <button
1165
+ class="gridlite-th-menu-btn"
1166
+ on:click|stopPropagation={() =>
1167
+ (columnMenuOpen = columnMenuOpen === col.name ? null : col.name)}
1168
+ title="Column options"
1169
+ type="button"
1170
+ >
1171
+ <svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1172
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
1173
+ </svg>
1174
+ </button>
1175
+ <ColumnMenu
1176
+ columnName={col.name}
1177
+ isOpen={columnMenuOpen === col.name}
1178
+ {sorting}
1179
+ canSort={features.sorting ?? false}
1180
+ canFilter={features.filtering ?? false}
1181
+ canGroup={features.grouping ?? false}
1182
+ onSort={handleColumnMenuSort}
1183
+ onFilter={handleColumnMenuFilter}
1184
+ onGroup={handleColumnMenuGroup}
1185
+ onHide={handleColumnMenuHide}
1186
+ onClose={() => (columnMenuOpen = null)}
1187
+ />
1188
1188
  </div>
1189
1189
  {#if features.columnResizing}
1190
1190
  <!-- svelte-ignore a11y-no-static-element-interactions -->
@@ -1401,6 +1401,7 @@ onDestroy(() => {
1401
1401
  <FilterBar
1402
1402
  {db}
1403
1403
  table={table ?? ''}
1404
+ source={query ?? ''}
1404
1405
  {columns}
1405
1406
  columnConfigs={mergedColumnConfigs}
1406
1407
  {allowedColumns}
@@ -1,7 +1,8 @@
1
- <script>import { quoteIdentifier } from "../query/builder.js";
1
+ <script>import { quoteIdentifier, resolveFrom } from "../query/builder.js";
2
2
  import FilterConditionComponent from "./FilterCondition.svelte";
3
3
  export let db;
4
- export let table;
4
+ export let table = "";
5
+ export let source = "";
5
6
  export let columns;
6
7
  export let columnConfigs = [];
7
8
  export let allowedColumns = [];
@@ -14,13 +15,14 @@ export let onExpandedChange = void 0;
14
15
  let columnValuesCache = /* @__PURE__ */ new Map();
15
16
  let numericRangeCache = /* @__PURE__ */ new Map();
16
17
  async function getColumnValues(columnName) {
17
- if (!columnName || !table) return [];
18
+ if (!columnName || !table && !source) return [];
18
19
  if (columnValuesCache.has(columnName)) {
19
20
  return columnValuesCache.get(columnName);
20
21
  }
21
22
  try {
23
+ const fromClause = resolveFrom(table || void 0, source || void 0);
22
24
  const quotedCol = quoteIdentifier(columnName, allowedColumns);
23
- const sql = `SELECT DISTINCT ${quotedCol}::TEXT AS val FROM ${quoteIdentifier(table)} WHERE ${quotedCol} IS NOT NULL ORDER BY val LIMIT 200`;
25
+ const sql = `SELECT DISTINCT ${quotedCol}::TEXT AS val FROM ${fromClause} WHERE ${quotedCol} IS NOT NULL ORDER BY val LIMIT 200`;
24
26
  const result = await db.query(sql);
25
27
  const values = result.rows.map((r) => r.val);
26
28
  columnValuesCache.set(columnName, values);
@@ -31,7 +33,7 @@ async function getColumnValues(columnName) {
31
33
  }
32
34
  }
33
35
  async function getNumericRange(columnName) {
34
- if (!columnName || !table) return null;
36
+ if (!columnName || !table && !source) return null;
35
37
  if (numericRangeCache.has(columnName)) {
36
38
  return numericRangeCache.get(columnName);
37
39
  }
@@ -43,8 +45,9 @@ async function getNumericRange(columnName) {
43
45
  return null;
44
46
  }
45
47
  try {
48
+ const fromClause = resolveFrom(table || void 0, source || void 0);
46
49
  const quotedCol = quoteIdentifier(columnName, allowedColumns);
47
- const sql = `SELECT MIN(${quotedCol})::NUMERIC AS min_val, MAX(${quotedCol})::NUMERIC AS max_val FROM ${quoteIdentifier(table)}`;
50
+ const sql = `SELECT MIN(${quotedCol})::NUMERIC AS min_val, MAX(${quotedCol})::NUMERIC AS max_val FROM ${fromClause}`;
48
51
  const result = await db.query(sql);
49
52
  const row = result.rows[0];
50
53
  if (row && row.min_val != null && row.max_val != null) {
@@ -10,7 +10,8 @@ import type { PGliteWithLive } from '../query/live.js';
10
10
  declare const __propDef: {
11
11
  props: {
12
12
  /** PGLite instance for running suggestion queries */ db: PGliteWithLive;
13
- /** Table name for suggestion queries */ table: string;
13
+ /** Table name for suggestion queries (mutually exclusive with `source`) */ table?: string;
14
+ /** Raw SQL subquery source for suggestion queries (mutually exclusive with `table`) */ source?: string;
14
15
  /** Introspected column metadata */ columns: ColumnMetadata[];
15
16
  /** Column config overrides (labels, dataType, selectOptions) */ columnConfigs?: ColumnConfig[];
16
17
  /** Allowed column names for query safety */ allowedColumns?: string[];
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export { default as ColumnMenu } from "./components/ColumnMenu.svelte";
9
9
  export { default as ColumnPicker } from "./components/ColumnPicker.svelte";
10
10
  export { default as RowDetailModal } from "./components/RowDetailModal.svelte";
11
11
  export type { GridLiteProps, GridConfig, GridFeatures, GridState, ColumnConfig, ColumnDataType, ColumnMetadata, FilterCondition, FilterOperator, FilterLogic, SortConfig, GroupConfig, AggregationConfig, AggregateFunction, ViewPreset, ClassNameMap, RowHeight, ColumnSpacing, ParameterizedQuery, ToolbarLayout, } from "./types.js";
12
- export { quoteIdentifier, buildWhereClause, buildOrderByClause, buildGroupByClause, buildPaginationClause, buildGlobalSearchClause, buildGroupSummaryQuery, buildGroupCountQuery, buildGroupDetailQuery, buildQuery, buildCountQuery, } from "./query/builder.js";
12
+ export { quoteIdentifier, resolveFrom, buildWhereClause, buildOrderByClause, buildGroupByClause, buildPaginationClause, buildGlobalSearchClause, buildGroupSummaryQuery, buildGroupCountQuery, buildGroupDetailQuery, buildQuery, buildCountQuery, } from "./query/builder.js";
13
13
  export type { QueryOptions, GroupSummaryOptions, GroupDetailOptions, } from "./query/builder.js";
14
14
  export { mapPostgresType, mapOidToDataType, introspectTable, getColumnNames, } from "./query/schema.js";
15
15
  export { createLiveQueryStore, createLiveQueryStoreFromQuery, } from "./query/live.js";
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ export { default as ColumnMenu } from "./components/ColumnMenu.svelte";
12
12
  export { default as ColumnPicker } from "./components/ColumnPicker.svelte";
13
13
  export { default as RowDetailModal } from "./components/RowDetailModal.svelte";
14
14
  // Query builder
15
- export { quoteIdentifier, buildWhereClause, buildOrderByClause, buildGroupByClause, buildPaginationClause, buildGlobalSearchClause, buildGroupSummaryQuery, buildGroupCountQuery, buildGroupDetailQuery, buildQuery, buildCountQuery, } from "./query/builder.js";
15
+ export { quoteIdentifier, resolveFrom, buildWhereClause, buildOrderByClause, buildGroupByClause, buildPaginationClause, buildGlobalSearchClause, buildGroupSummaryQuery, buildGroupCountQuery, buildGroupDetailQuery, buildQuery, buildCountQuery, } from "./query/builder.js";
16
16
  // Schema introspection
17
17
  export { mapPostgresType, mapOidToDataType, introspectTable, getColumnNames, } from "./query/schema.js";
18
18
  // Live query store
@@ -9,6 +9,12 @@
9
9
  * to prevent SQL injection via identifier manipulation.
10
10
  */
11
11
  import type { FilterCondition, FilterLogic, SortConfig, GroupConfig, ParameterizedQuery } from "../types.js";
12
+ /**
13
+ * Resolve a FROM clause from either a table name or a raw SQL source.
14
+ * When `source` is provided, it's used as a subquery: `(source) AS _gridlite_sub`.
15
+ * When `table` is provided, it's quoted as an identifier: `"table"`.
16
+ */
17
+ export declare function resolveFrom(table?: string, source?: string): string;
12
18
  /**
13
19
  * Validate and quote a column name as a SQL identifier.
14
20
  * Throws if the name is not a valid identifier.
@@ -59,8 +65,10 @@ export declare function buildPaginationClause(page: number, pageSize: number): s
59
65
  */
60
66
  export declare function buildGlobalSearchClause(searchTerm: string, textColumns: string[], paramOffset?: number, allowedColumns?: string[]): ParameterizedQuery;
61
67
  export interface GroupSummaryOptions {
62
- /** Table name */
63
- table: string;
68
+ /** Table name (mutually exclusive with `source`) */
69
+ table?: string;
70
+ /** Raw SQL subquery source (mutually exclusive with `table`) */
71
+ source?: string;
64
72
  /** Group columns (max 3) */
65
73
  grouping: GroupConfig[];
66
74
  /** Filter conditions to apply before grouping */
@@ -94,8 +102,10 @@ export declare function buildGroupSummaryQuery(options: GroupSummaryOptions): Pa
94
102
  */
95
103
  export declare function buildGroupCountQuery(options: GroupSummaryOptions): ParameterizedQuery;
96
104
  export interface GroupDetailOptions {
97
- /** Table name */
98
- table: string;
105
+ /** Table name (mutually exclusive with `source`) */
106
+ table?: string;
107
+ /** Raw SQL subquery source (mutually exclusive with `table`) */
108
+ source?: string;
99
109
  /** Values to match for parent groups: [{ column: "department", value: "Engineering" }] */
100
110
  groupValues: {
101
111
  column: string;
@@ -125,8 +135,10 @@ export interface GroupDetailOptions {
125
135
  */
126
136
  export declare function buildGroupDetailQuery(options: GroupDetailOptions): ParameterizedQuery;
127
137
  export interface QueryOptions {
128
- /** Table name to query */
129
- table: string;
138
+ /** Table name to query (mutually exclusive with `source`) */
139
+ table?: string;
140
+ /** Raw SQL subquery source (mutually exclusive with `table`) */
141
+ source?: string;
130
142
  /** Filter conditions */
131
143
  filters?: FilterCondition[];
132
144
  /** Filter logic (and/or) */
@@ -8,6 +8,21 @@
8
8
  * Column names are validated against an allowlist (from schema introspection)
9
9
  * to prevent SQL injection via identifier manipulation.
10
10
  */
11
+ // ─── Source Resolution ──────────────────────────────────────────────────────
12
+ /**
13
+ * Resolve a FROM clause from either a table name or a raw SQL source.
14
+ * When `source` is provided, it's used as a subquery: `(source) AS _gridlite_sub`.
15
+ * When `table` is provided, it's quoted as an identifier: `"table"`.
16
+ */
17
+ export function resolveFrom(table, source) {
18
+ if (source) {
19
+ return `(${source}) AS _gridlite_sub`;
20
+ }
21
+ if (table) {
22
+ return quoteIdentifier(table);
23
+ }
24
+ throw new Error("Either `table` or `source` must be provided");
25
+ }
11
26
  // ─── Column Name Validation ─────────────────────────────────────────────────
12
27
  /**
13
28
  * Valid SQL identifier pattern: letters, digits, underscores.
@@ -222,11 +237,11 @@ export function buildGlobalSearchClause(searchTerm, textColumns, paramOffset = 0
222
237
  * FROM "employees" WHERE ... GROUP BY "department" ORDER BY "department"
223
238
  */
224
239
  export function buildGroupSummaryQuery(options) {
225
- const { table, grouping, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, sorting = [], page, pageSize, } = options;
240
+ const { table, source, grouping, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, sorting = [], page, pageSize, } = options;
226
241
  if (grouping.length === 0) {
227
242
  throw new Error("buildGroupSummaryQuery requires at least one group");
228
243
  }
229
- const tableName = quoteIdentifier(table);
244
+ const fromClause = resolveFrom(table, source);
230
245
  const { selectColumns, groupBy } = buildGroupByClause(grouping, allowedColumns);
231
246
  // WHERE from filters
232
247
  const where = buildWhereClause(filters, filterLogic, 0, allowedColumns);
@@ -260,7 +275,7 @@ export function buildGroupSummaryQuery(options) {
260
275
  : "";
261
276
  const parts = [
262
277
  `SELECT ${selectColumns}`,
263
- `FROM ${tableName}`,
278
+ `FROM ${fromClause}`,
264
279
  whereSQL,
265
280
  groupBy,
266
281
  orderBy,
@@ -272,11 +287,11 @@ export function buildGroupSummaryQuery(options) {
272
287
  * Build a count query for group summaries (how many groups exist).
273
288
  */
274
289
  export function buildGroupCountQuery(options) {
275
- const { table, grouping, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, } = options;
290
+ const { table, source, grouping, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, } = options;
276
291
  if (grouping.length === 0) {
277
292
  throw new Error("buildGroupCountQuery requires at least one group");
278
293
  }
279
- const tableName = quoteIdentifier(table);
294
+ const fromClause = resolveFrom(table, source);
280
295
  const groupCols = grouping.map((g) => quoteIdentifier(g.column, allowedColumns));
281
296
  const where = buildWhereClause(filters, filterLogic, 0, allowedColumns);
282
297
  const searchCols = searchColumns ?? allowedColumns ?? [];
@@ -293,7 +308,7 @@ export function buildGroupCountQuery(options) {
293
308
  whereSQL = `WHERE ${globalSearchClause.sql}`;
294
309
  }
295
310
  const parts = [
296
- `SELECT COUNT(*) AS "total" FROM (SELECT 1 FROM ${tableName}`,
311
+ `SELECT COUNT(*) AS "total" FROM (SELECT 1 FROM ${fromClause}`,
297
312
  whereSQL,
298
313
  `GROUP BY ${groupCols.join(", ")}`,
299
314
  `) AS "_groups"`,
@@ -310,11 +325,11 @@ export function buildGroupCountQuery(options) {
310
325
  * SELECT * FROM "employees" WHERE ... AND "department" = $N ORDER BY ...
311
326
  */
312
327
  export function buildGroupDetailQuery(options) {
313
- const { table, groupValues, filters = [], filterLogic = "and", sorting = [], allowedColumns, globalSearch, searchColumns, } = options;
328
+ const { table, source, groupValues, filters = [], filterLogic = "and", sorting = [], allowedColumns, globalSearch, searchColumns, } = options;
314
329
  if (groupValues.length === 0) {
315
330
  throw new Error("buildGroupDetailQuery requires at least one group value");
316
331
  }
317
- const tableName = quoteIdentifier(table);
332
+ const fromClause = resolveFrom(table, source);
318
333
  // Base WHERE from filters
319
334
  const where = buildWhereClause(filters, filterLogic, 0, allowedColumns);
320
335
  // Global search
@@ -345,7 +360,7 @@ export function buildGroupDetailQuery(options) {
345
360
  const allWhereParts = [...baseWhereParts, ...groupConstraints];
346
361
  const whereSQL = allWhereParts.length > 0 ? `WHERE ${allWhereParts.join(" AND ")}` : "";
347
362
  const orderBy = buildOrderByClause(sorting, allowedColumns);
348
- const parts = [`SELECT *`, `FROM ${tableName}`, whereSQL, orderBy].filter(Boolean);
363
+ const parts = [`SELECT *`, `FROM ${fromClause}`, whereSQL, orderBy].filter(Boolean);
349
364
  return { sql: parts.join(" "), params: allParams };
350
365
  }
351
366
  /**
@@ -355,8 +370,8 @@ export function buildGroupDetailQuery(options) {
355
370
  * WHERE, ORDER BY, GROUP BY, and LIMIT/OFFSET clauses into a single query.
356
371
  */
357
372
  export function buildQuery(options) {
358
- const { table, filters = [], filterLogic = "and", sorting = [], grouping = [], page, pageSize, allowedColumns, globalSearch, searchColumns, } = options;
359
- const tableName = quoteIdentifier(table);
373
+ const { table, source, filters = [], filterLogic = "and", sorting = [], grouping = [], page, pageSize, allowedColumns, globalSearch, searchColumns, } = options;
374
+ const fromClause = resolveFrom(table, source);
360
375
  // GROUP BY affects SELECT columns
361
376
  const { selectColumns, groupBy } = buildGroupByClause(grouping, allowedColumns);
362
377
  // WHERE clause from filters
@@ -386,7 +401,7 @@ export function buildQuery(options) {
386
401
  // Compose
387
402
  const parts = [
388
403
  `SELECT ${selectColumns}`,
389
- `FROM ${tableName}`,
404
+ `FROM ${fromClause}`,
390
405
  whereSQL,
391
406
  groupBy,
392
407
  orderBy,
@@ -402,8 +417,8 @@ export function buildQuery(options) {
402
417
  * Uses the same filters but no sorting/grouping/pagination.
403
418
  */
404
419
  export function buildCountQuery(options) {
405
- const { table, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, } = options;
406
- const tableName = quoteIdentifier(table);
420
+ const { table, source, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, } = options;
421
+ const fromClause = resolveFrom(table, source);
407
422
  const where = buildWhereClause(filters, filterLogic, 0, allowedColumns);
408
423
  // Global search clause
409
424
  const searchCols = searchColumns ?? allowedColumns ?? [];
@@ -422,7 +437,7 @@ export function buildCountQuery(options) {
422
437
  }
423
438
  const parts = [
424
439
  'SELECT COUNT(*) AS "total"',
425
- `FROM ${tableName}`,
440
+ `FROM ${fromClause}`,
426
441
  whereSQL,
427
442
  ].filter(Boolean);
428
443
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shotleybuilder/svelte-gridlite-kit",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "A SQL-native data grid component for Svelte and SvelteKit, powered by PGLite",
5
5
  "author": "Sertantai",
6
6
  "license": "MIT",