@shotleybuilder/svelte-gridlite-kit 0.1.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.
Files changed (43) hide show
  1. package/README.md +260 -0
  2. package/dist/GridLite.svelte +1361 -0
  3. package/dist/GridLite.svelte.d.ts +42 -0
  4. package/dist/components/CellContextMenu.svelte +209 -0
  5. package/dist/components/CellContextMenu.svelte.d.ts +28 -0
  6. package/dist/components/ColumnMenu.svelte +234 -0
  7. package/dist/components/ColumnMenu.svelte.d.ts +29 -0
  8. package/dist/components/ColumnPicker.svelte +403 -0
  9. package/dist/components/ColumnPicker.svelte.d.ts +29 -0
  10. package/dist/components/FilterBar.svelte +390 -0
  11. package/dist/components/FilterBar.svelte.d.ts +38 -0
  12. package/dist/components/FilterCondition.svelte +643 -0
  13. package/dist/components/FilterCondition.svelte.d.ts +35 -0
  14. package/dist/components/GroupBar.svelte +463 -0
  15. package/dist/components/GroupBar.svelte.d.ts +33 -0
  16. package/dist/components/RowDetailModal.svelte +213 -0
  17. package/dist/components/RowDetailModal.svelte.d.ts +25 -0
  18. package/dist/components/SortBar.svelte +232 -0
  19. package/dist/components/SortBar.svelte.d.ts +30 -0
  20. package/dist/components/SortCondition.svelte +129 -0
  21. package/dist/components/SortCondition.svelte.d.ts +30 -0
  22. package/dist/index.d.ts +23 -0
  23. package/dist/index.js +29 -0
  24. package/dist/query/builder.d.ts +160 -0
  25. package/dist/query/builder.js +432 -0
  26. package/dist/query/live.d.ts +50 -0
  27. package/dist/query/live.js +118 -0
  28. package/dist/query/schema.d.ts +30 -0
  29. package/dist/query/schema.js +75 -0
  30. package/dist/state/migrations.d.ts +29 -0
  31. package/dist/state/migrations.js +113 -0
  32. package/dist/state/views.d.ts +54 -0
  33. package/dist/state/views.js +130 -0
  34. package/dist/styles/gridlite.css +966 -0
  35. package/dist/types.d.ts +164 -0
  36. package/dist/types.js +2 -0
  37. package/dist/utils/filters.d.ts +14 -0
  38. package/dist/utils/filters.js +49 -0
  39. package/dist/utils/formatters.d.ts +16 -0
  40. package/dist/utils/formatters.js +39 -0
  41. package/dist/utils/fuzzy.d.ts +47 -0
  42. package/dist/utils/fuzzy.js +142 -0
  43. package/package.json +76 -0
@@ -0,0 +1,432 @@
1
+ /**
2
+ * SQL Query Builder for GridLite
3
+ *
4
+ * Translates FilterCondition[], SortConfig[], GroupConfig[], and pagination
5
+ * into parameterized SQL queries. All user input is parameterized — never
6
+ * interpolated into SQL strings.
7
+ *
8
+ * Column names are validated against an allowlist (from schema introspection)
9
+ * to prevent SQL injection via identifier manipulation.
10
+ */
11
+ // ─── Column Name Validation ─────────────────────────────────────────────────
12
+ /**
13
+ * Valid SQL identifier pattern: letters, digits, underscores.
14
+ * Rejects anything that could be used for SQL injection via identifiers.
15
+ */
16
+ const VALID_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
17
+ /**
18
+ * Validate and quote a column name as a SQL identifier.
19
+ * Throws if the name is not a valid identifier.
20
+ *
21
+ * When `allowedColumns` is provided, also checks the name is in the allowlist.
22
+ */
23
+ export function quoteIdentifier(name, allowedColumns) {
24
+ if (!VALID_IDENTIFIER.test(name)) {
25
+ throw new Error(`Invalid column name: ${JSON.stringify(name)}`);
26
+ }
27
+ if (allowedColumns && !allowedColumns.includes(name)) {
28
+ throw new Error(`Column not found: ${JSON.stringify(name)}`);
29
+ }
30
+ return `"${name}"`;
31
+ }
32
+ // ─── WHERE Clause Builder ───────────────────────────────────────────────────
33
+ /**
34
+ * Build a WHERE clause from filter conditions.
35
+ *
36
+ * Returns `{ sql: string, params: unknown[] }` where sql is empty string
37
+ * if there are no valid conditions.
38
+ *
39
+ * @param conditions - Filter conditions from the UI
40
+ * @param logic - 'and' or 'or' between conditions
41
+ * @param paramOffset - Starting parameter index (for $1, $2, ...) when composing with other clauses
42
+ * @param allowedColumns - Optional allowlist of column names from schema introspection
43
+ */
44
+ export function buildWhereClause(conditions, logic = "and", paramOffset = 0, allowedColumns) {
45
+ // Filter out invalid conditions
46
+ const valid = conditions.filter((c) => c.field &&
47
+ (c.operator === "is_empty" ||
48
+ c.operator === "is_not_empty" ||
49
+ (c.value !== null && c.value !== undefined && c.value !== "")));
50
+ if (valid.length === 0) {
51
+ return { sql: "", params: [] };
52
+ }
53
+ const parts = [];
54
+ const params = [];
55
+ let paramIndex = paramOffset + 1; // PostgreSQL params are 1-indexed
56
+ for (const condition of valid) {
57
+ const col = quoteIdentifier(condition.field, allowedColumns);
58
+ const result = buildConditionSQL(col, condition, paramIndex);
59
+ parts.push(result.sql);
60
+ params.push(...result.params);
61
+ paramIndex += result.params.length;
62
+ }
63
+ const joiner = logic === "or" ? " OR " : " AND ";
64
+ const sql = `WHERE ${parts.join(joiner)}`;
65
+ return { sql, params };
66
+ }
67
+ /**
68
+ * Build SQL for a single filter condition.
69
+ */
70
+ function buildConditionSQL(quotedCol, condition, paramIndex) {
71
+ const p = (offset = 0) => `$${paramIndex + offset}`;
72
+ switch (condition.operator) {
73
+ // String operators
74
+ case "equals":
75
+ return { sql: `${quotedCol} = ${p()}`, params: [condition.value] };
76
+ case "not_equals":
77
+ return { sql: `${quotedCol} != ${p()}`, params: [condition.value] };
78
+ case "contains":
79
+ return {
80
+ sql: `${quotedCol} ILIKE '%' || ${p()} || '%'`,
81
+ params: [condition.value],
82
+ };
83
+ case "not_contains":
84
+ return {
85
+ sql: `${quotedCol} NOT ILIKE '%' || ${p()} || '%'`,
86
+ params: [condition.value],
87
+ };
88
+ case "starts_with":
89
+ return {
90
+ sql: `${quotedCol} ILIKE ${p()} || '%'`,
91
+ params: [condition.value],
92
+ };
93
+ case "ends_with":
94
+ return {
95
+ sql: `${quotedCol} ILIKE '%' || ${p()}`,
96
+ params: [condition.value],
97
+ };
98
+ case "is_empty":
99
+ return { sql: `(${quotedCol} IS NULL OR ${quotedCol} = '')`, params: [] };
100
+ case "is_not_empty":
101
+ return {
102
+ sql: `(${quotedCol} IS NOT NULL AND ${quotedCol} != '')`,
103
+ params: [],
104
+ };
105
+ // Numeric operators
106
+ case "greater_than":
107
+ return { sql: `${quotedCol} > ${p()}`, params: [condition.value] };
108
+ case "less_than":
109
+ return { sql: `${quotedCol} < ${p()}`, params: [condition.value] };
110
+ case "greater_or_equal":
111
+ return { sql: `${quotedCol} >= ${p()}`, params: [condition.value] };
112
+ case "less_or_equal":
113
+ return { sql: `${quotedCol} <= ${p()}`, params: [condition.value] };
114
+ // Date operators
115
+ case "is_before":
116
+ return { sql: `${quotedCol} < ${p()}`, params: [condition.value] };
117
+ case "is_after":
118
+ return { sql: `${quotedCol} > ${p()}`, params: [condition.value] };
119
+ default:
120
+ throw new Error(`Unknown filter operator: ${condition.operator}`);
121
+ }
122
+ }
123
+ // ─── ORDER BY Clause Builder ────────────────────────────────────────────────
124
+ /**
125
+ * Build an ORDER BY clause from sort configurations.
126
+ */
127
+ export function buildOrderByClause(sorting, allowedColumns) {
128
+ if (sorting.length === 0)
129
+ return "";
130
+ const parts = sorting.map((s) => {
131
+ const col = quoteIdentifier(s.column, allowedColumns);
132
+ const dir = s.direction === "desc" ? "DESC" : "ASC";
133
+ return `${col} ${dir}`;
134
+ });
135
+ return `ORDER BY ${parts.join(", ")}`;
136
+ }
137
+ // ─── GROUP BY Clause Builder ────────────────────────────────────────────────
138
+ const VALID_AGGREGATES = [
139
+ "count",
140
+ "sum",
141
+ "avg",
142
+ "min",
143
+ "max",
144
+ ];
145
+ /**
146
+ * Build GROUP BY clause and adjust SELECT for grouped queries.
147
+ *
148
+ * Returns the GROUP BY clause and the SELECT column list.
149
+ */
150
+ export function buildGroupByClause(grouping, allowedColumns) {
151
+ if (grouping.length === 0) {
152
+ return { selectColumns: "*", groupBy: "" };
153
+ }
154
+ const groupCols = grouping.map((g) => quoteIdentifier(g.column, allowedColumns));
155
+ const aggParts = [];
156
+ for (const group of grouping) {
157
+ if (group.aggregations) {
158
+ for (const agg of group.aggregations) {
159
+ if (!VALID_AGGREGATES.includes(agg.function)) {
160
+ throw new Error(`Invalid aggregate function: ${agg.function}`);
161
+ }
162
+ const aggCol = quoteIdentifier(agg.column, allowedColumns);
163
+ const alias = agg.alias
164
+ ? quoteIdentifier(agg.alias)
165
+ : `"${agg.function}_${agg.column}"`;
166
+ aggParts.push(`${agg.function.toUpperCase()}(${aggCol}) AS ${alias}`);
167
+ }
168
+ }
169
+ }
170
+ // Always include COUNT(*) for grouped results
171
+ aggParts.push('COUNT(*) AS "_count"');
172
+ const selectColumns = [...groupCols, ...aggParts].join(", ");
173
+ const groupBy = `GROUP BY ${groupCols.join(", ")}`;
174
+ return { selectColumns, groupBy };
175
+ }
176
+ // ─── Pagination ─────────────────────────────────────────────────────────────
177
+ /**
178
+ * Build LIMIT/OFFSET clause for pagination.
179
+ */
180
+ export function buildPaginationClause(page, pageSize) {
181
+ if (pageSize <= 0)
182
+ throw new Error("pageSize must be positive");
183
+ if (page < 0)
184
+ throw new Error("page must be non-negative");
185
+ const offset = page * pageSize;
186
+ return `LIMIT ${pageSize} OFFSET ${offset}`;
187
+ }
188
+ // ─── Global Search ──────────────────────────────────────────────────────────
189
+ /**
190
+ * Build a WHERE-compatible clause for global search across text columns.
191
+ *
192
+ * Generates `(col1::text ILIKE '%' || $N || '%' OR col2::text ILIKE ...)`
193
+ * using a single parameter for the search term. The `::text` cast allows
194
+ * searching non-text columns (numbers, dates, booleans) as strings.
195
+ *
196
+ * @param searchTerm - The search string
197
+ * @param textColumns - Column names to search across
198
+ * @param paramOffset - Starting parameter index
199
+ * @param allowedColumns - Optional allowlist
200
+ */
201
+ export function buildGlobalSearchClause(searchTerm, textColumns, paramOffset = 0, allowedColumns) {
202
+ if (!searchTerm || textColumns.length === 0) {
203
+ return { sql: "", params: [] };
204
+ }
205
+ const paramIndex = paramOffset + 1;
206
+ const p = `$${paramIndex}`;
207
+ const parts = textColumns.map((col) => {
208
+ const quoted = quoteIdentifier(col, allowedColumns);
209
+ return `${quoted}::text ILIKE '%' || ${p} || '%'`;
210
+ });
211
+ return {
212
+ sql: `(${parts.join(" OR ")})`,
213
+ params: [searchTerm],
214
+ };
215
+ }
216
+ /**
217
+ * Build a query that returns distinct group values with COUNT(*) and
218
+ * optional aggregations. Used for the group header rows.
219
+ *
220
+ * Example output for grouping by department with avg(salary):
221
+ * SELECT "department", COUNT(*) AS "_count", AVG("salary") AS "avg_salary"
222
+ * FROM "employees" WHERE ... GROUP BY "department" ORDER BY "department"
223
+ */
224
+ export function buildGroupSummaryQuery(options) {
225
+ const { table, grouping, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, sorting = [], page, pageSize, } = options;
226
+ if (grouping.length === 0) {
227
+ throw new Error("buildGroupSummaryQuery requires at least one group");
228
+ }
229
+ const tableName = quoteIdentifier(table);
230
+ const { selectColumns, groupBy } = buildGroupByClause(grouping, allowedColumns);
231
+ // WHERE from filters
232
+ const where = buildWhereClause(filters, filterLogic, 0, allowedColumns);
233
+ // Global search
234
+ const searchCols = searchColumns ?? allowedColumns ?? [];
235
+ const globalSearchClause = buildGlobalSearchClause(globalSearch ?? "", searchCols, where.params.length, allowedColumns);
236
+ const allParams = [...where.params, ...globalSearchClause.params];
237
+ let whereSQL = "";
238
+ if (where.sql && globalSearchClause.sql) {
239
+ whereSQL = `WHERE ${where.sql.replace(/^WHERE /, "")} AND ${globalSearchClause.sql}`;
240
+ }
241
+ else if (where.sql) {
242
+ whereSQL = where.sql;
243
+ }
244
+ else if (globalSearchClause.sql) {
245
+ whereSQL = `WHERE ${globalSearchClause.sql}`;
246
+ }
247
+ // Sort by group columns if no explicit sort, or by specified sort
248
+ let orderBy = "";
249
+ if (sorting.length > 0) {
250
+ orderBy = buildOrderByClause(sorting, allowedColumns);
251
+ }
252
+ else {
253
+ // Default: sort by group columns ascending
254
+ const groupCols = grouping.map((g) => quoteIdentifier(g.column, allowedColumns));
255
+ orderBy = `ORDER BY ${groupCols.join(", ")}`;
256
+ }
257
+ // Pagination on groups
258
+ const pagination = page !== undefined && pageSize !== undefined
259
+ ? buildPaginationClause(page, pageSize)
260
+ : "";
261
+ const parts = [
262
+ `SELECT ${selectColumns}`,
263
+ `FROM ${tableName}`,
264
+ whereSQL,
265
+ groupBy,
266
+ orderBy,
267
+ pagination,
268
+ ].filter(Boolean);
269
+ return { sql: parts.join(" "), params: allParams };
270
+ }
271
+ /**
272
+ * Build a count query for group summaries (how many groups exist).
273
+ */
274
+ export function buildGroupCountQuery(options) {
275
+ const { table, grouping, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, } = options;
276
+ if (grouping.length === 0) {
277
+ throw new Error("buildGroupCountQuery requires at least one group");
278
+ }
279
+ const tableName = quoteIdentifier(table);
280
+ const groupCols = grouping.map((g) => quoteIdentifier(g.column, allowedColumns));
281
+ const where = buildWhereClause(filters, filterLogic, 0, allowedColumns);
282
+ const searchCols = searchColumns ?? allowedColumns ?? [];
283
+ const globalSearchClause = buildGlobalSearchClause(globalSearch ?? "", searchCols, where.params.length, allowedColumns);
284
+ const allParams = [...where.params, ...globalSearchClause.params];
285
+ let whereSQL = "";
286
+ if (where.sql && globalSearchClause.sql) {
287
+ whereSQL = `WHERE ${where.sql.replace(/^WHERE /, "")} AND ${globalSearchClause.sql}`;
288
+ }
289
+ else if (where.sql) {
290
+ whereSQL = where.sql;
291
+ }
292
+ else if (globalSearchClause.sql) {
293
+ whereSQL = `WHERE ${globalSearchClause.sql}`;
294
+ }
295
+ const parts = [
296
+ `SELECT COUNT(*) AS "total" FROM (SELECT 1 FROM ${tableName}`,
297
+ whereSQL,
298
+ `GROUP BY ${groupCols.join(", ")}`,
299
+ `) AS "_groups"`,
300
+ ].filter(Boolean);
301
+ return { sql: parts.join(" "), params: allParams };
302
+ }
303
+ /**
304
+ * Build a query that returns the detail rows for an expanded group.
305
+ *
306
+ * Adds WHERE constraints for each group column value on top of any
307
+ * existing filters. Used when a user expands a group header.
308
+ *
309
+ * Example: grouping by department, expanded "Engineering":
310
+ * SELECT * FROM "employees" WHERE ... AND "department" = $N ORDER BY ...
311
+ */
312
+ export function buildGroupDetailQuery(options) {
313
+ const { table, groupValues, filters = [], filterLogic = "and", sorting = [], allowedColumns, globalSearch, searchColumns, } = options;
314
+ if (groupValues.length === 0) {
315
+ throw new Error("buildGroupDetailQuery requires at least one group value");
316
+ }
317
+ const tableName = quoteIdentifier(table);
318
+ // Base WHERE from filters
319
+ const where = buildWhereClause(filters, filterLogic, 0, allowedColumns);
320
+ // Global search
321
+ const searchCols = searchColumns ?? allowedColumns ?? [];
322
+ const globalSearchClause = buildGlobalSearchClause(globalSearch ?? "", searchCols, where.params.length, allowedColumns);
323
+ let allParams = [...where.params, ...globalSearchClause.params];
324
+ // Build the combined WHERE from filters + global search
325
+ let baseWhereParts = [];
326
+ if (where.sql) {
327
+ baseWhereParts.push(where.sql.replace(/^WHERE /, ""));
328
+ }
329
+ if (globalSearchClause.sql) {
330
+ baseWhereParts.push(globalSearchClause.sql);
331
+ }
332
+ // Add group value constraints
333
+ const groupConstraints = [];
334
+ for (const gv of groupValues) {
335
+ const col = quoteIdentifier(gv.column, allowedColumns);
336
+ const paramIdx = allParams.length + 1;
337
+ if (gv.value === null) {
338
+ groupConstraints.push(`${col} IS NULL`);
339
+ }
340
+ else {
341
+ groupConstraints.push(`${col} = $${paramIdx}`);
342
+ allParams = [...allParams, gv.value];
343
+ }
344
+ }
345
+ const allWhereParts = [...baseWhereParts, ...groupConstraints];
346
+ const whereSQL = allWhereParts.length > 0 ? `WHERE ${allWhereParts.join(" AND ")}` : "";
347
+ const orderBy = buildOrderByClause(sorting, allowedColumns);
348
+ const parts = [`SELECT *`, `FROM ${tableName}`, whereSQL, orderBy].filter(Boolean);
349
+ return { sql: parts.join(" "), params: allParams };
350
+ }
351
+ /**
352
+ * Build a complete parameterized SELECT query from grid state.
353
+ *
354
+ * This is the main entry point for the query builder. It composes
355
+ * WHERE, ORDER BY, GROUP BY, and LIMIT/OFFSET clauses into a single query.
356
+ */
357
+ export function buildQuery(options) {
358
+ const { table, filters = [], filterLogic = "and", sorting = [], grouping = [], page, pageSize, allowedColumns, globalSearch, searchColumns, } = options;
359
+ const tableName = quoteIdentifier(table);
360
+ // GROUP BY affects SELECT columns
361
+ const { selectColumns, groupBy } = buildGroupByClause(grouping, allowedColumns);
362
+ // WHERE clause from filters
363
+ const where = buildWhereClause(filters, filterLogic, 0, allowedColumns);
364
+ // Global search clause
365
+ const searchCols = searchColumns ?? allowedColumns ?? [];
366
+ const globalSearchClause = buildGlobalSearchClause(globalSearch ?? "", searchCols, where.params.length, allowedColumns);
367
+ // Combine WHERE and global search
368
+ const allParams = [...where.params, ...globalSearchClause.params];
369
+ let whereSQL = "";
370
+ if (where.sql && globalSearchClause.sql) {
371
+ // Strip "WHERE " prefix from where.sql, combine with AND
372
+ whereSQL = `WHERE ${where.sql.replace(/^WHERE /, "")} AND ${globalSearchClause.sql}`;
373
+ }
374
+ else if (where.sql) {
375
+ whereSQL = where.sql;
376
+ }
377
+ else if (globalSearchClause.sql) {
378
+ whereSQL = `WHERE ${globalSearchClause.sql}`;
379
+ }
380
+ // ORDER BY clause
381
+ const orderBy = buildOrderByClause(sorting, allowedColumns);
382
+ // LIMIT/OFFSET clause
383
+ const pagination = page !== undefined && pageSize !== undefined
384
+ ? buildPaginationClause(page, pageSize)
385
+ : "";
386
+ // Compose
387
+ const parts = [
388
+ `SELECT ${selectColumns}`,
389
+ `FROM ${tableName}`,
390
+ whereSQL,
391
+ groupBy,
392
+ orderBy,
393
+ pagination,
394
+ ].filter(Boolean);
395
+ return {
396
+ sql: parts.join(" "),
397
+ params: allParams,
398
+ };
399
+ }
400
+ /**
401
+ * Build a COUNT query for pagination total.
402
+ * Uses the same filters but no sorting/grouping/pagination.
403
+ */
404
+ export function buildCountQuery(options) {
405
+ const { table, filters = [], filterLogic = "and", allowedColumns, globalSearch, searchColumns, } = options;
406
+ const tableName = quoteIdentifier(table);
407
+ const where = buildWhereClause(filters, filterLogic, 0, allowedColumns);
408
+ // Global search clause
409
+ const searchCols = searchColumns ?? allowedColumns ?? [];
410
+ const globalSearchClause = buildGlobalSearchClause(globalSearch ?? "", searchCols, where.params.length, allowedColumns);
411
+ // Combine WHERE and global search
412
+ const allParams = [...where.params, ...globalSearchClause.params];
413
+ let whereSQL = "";
414
+ if (where.sql && globalSearchClause.sql) {
415
+ whereSQL = `WHERE ${where.sql.replace(/^WHERE /, "")} AND ${globalSearchClause.sql}`;
416
+ }
417
+ else if (where.sql) {
418
+ whereSQL = where.sql;
419
+ }
420
+ else if (globalSearchClause.sql) {
421
+ whereSQL = `WHERE ${globalSearchClause.sql}`;
422
+ }
423
+ const parts = [
424
+ 'SELECT COUNT(*) AS "total"',
425
+ `FROM ${tableName}`,
426
+ whereSQL,
427
+ ].filter(Boolean);
428
+ return {
429
+ sql: parts.join(" "),
430
+ params: allParams,
431
+ };
432
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Svelte Store Wrapper for PGLite Live Queries
3
+ *
4
+ * Bridges PGLite's live.query() API with Svelte's store contract.
5
+ * Provides loading/error/data states and handles subscription lifecycle.
6
+ *
7
+ * Usage:
8
+ * const store = createLiveQueryStore(db, 'SELECT * FROM users WHERE active = $1', [true]);
9
+ * // In Svelte: $store.rows, $store.loading, $store.error
10
+ * // Call store.destroy() on component unmount
11
+ */
12
+ import type { PGlite } from '@electric-sql/pglite';
13
+ import type { LiveNamespace } from '@electric-sql/pglite/live';
14
+ import type { ParameterizedQuery } from '../types.js';
15
+ export interface LiveQueryState<T = Record<string, unknown>> {
16
+ rows: T[];
17
+ fields: {
18
+ name: string;
19
+ dataTypeID: number;
20
+ }[];
21
+ totalCount?: number;
22
+ loading: boolean;
23
+ error: Error | null;
24
+ }
25
+ export interface LiveQueryStore<T = Record<string, unknown>> {
26
+ subscribe: (callback: (state: LiveQueryState<T>) => void) => () => void;
27
+ refresh: () => Promise<void>;
28
+ update: (query: string, params?: unknown[]) => Promise<void>;
29
+ destroy: () => Promise<void>;
30
+ }
31
+ /** A PGLite instance that has the live extension loaded */
32
+ export type PGliteWithLive = PGlite & {
33
+ live: LiveNamespace;
34
+ };
35
+ /**
36
+ * Create a Svelte-compatible store backed by a PGLite live query.
37
+ *
38
+ * The store follows Svelte's store contract (has a `subscribe` method that
39
+ * returns an unsubscribe function). It also exposes `refresh`, `update`,
40
+ * and `destroy` methods.
41
+ *
42
+ * @param db - PGLite instance with the live extension loaded
43
+ * @param query - SQL query string with $1, $2, ... parameters
44
+ * @param params - Parameter values for the query
45
+ */
46
+ export declare function createLiveQueryStore<T = Record<string, unknown>>(db: PGliteWithLive, query: string, params?: unknown[]): LiveQueryStore<T>;
47
+ /**
48
+ * Convenience: create a live query store from a ParameterizedQuery object.
49
+ */
50
+ export declare function createLiveQueryStoreFromQuery<T = Record<string, unknown>>(db: PGliteWithLive, parameterizedQuery: ParameterizedQuery): LiveQueryStore<T>;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Svelte Store Wrapper for PGLite Live Queries
3
+ *
4
+ * Bridges PGLite's live.query() API with Svelte's store contract.
5
+ * Provides loading/error/data states and handles subscription lifecycle.
6
+ *
7
+ * Usage:
8
+ * const store = createLiveQueryStore(db, 'SELECT * FROM users WHERE active = $1', [true]);
9
+ * // In Svelte: $store.rows, $store.loading, $store.error
10
+ * // Call store.destroy() on component unmount
11
+ */
12
+ // ─── Store Factory ──────────────────────────────────────────────────────────
13
+ /**
14
+ * Create a Svelte-compatible store backed by a PGLite live query.
15
+ *
16
+ * The store follows Svelte's store contract (has a `subscribe` method that
17
+ * returns an unsubscribe function). It also exposes `refresh`, `update`,
18
+ * and `destroy` methods.
19
+ *
20
+ * @param db - PGLite instance with the live extension loaded
21
+ * @param query - SQL query string with $1, $2, ... parameters
22
+ * @param params - Parameter values for the query
23
+ */
24
+ export function createLiveQueryStore(db, query, params = []) {
25
+ let state = {
26
+ rows: [],
27
+ fields: [],
28
+ loading: true,
29
+ error: null
30
+ };
31
+ const subscribers = new Set();
32
+ // Internal unsubscribe from PGLite live query
33
+ let liveUnsubscribe = null;
34
+ let liveRefresh = null;
35
+ let destroyed = false;
36
+ function notify() {
37
+ for (const cb of subscribers) {
38
+ cb(state);
39
+ }
40
+ }
41
+ function setState(partial) {
42
+ state = { ...state, ...partial };
43
+ notify();
44
+ }
45
+ function handleResults(results) {
46
+ setState({
47
+ rows: results.rows,
48
+ fields: results.fields,
49
+ totalCount: results.totalCount,
50
+ loading: false,
51
+ error: null
52
+ });
53
+ }
54
+ async function setupLiveQuery(sql, queryParams) {
55
+ // Tear down existing subscription
56
+ if (liveUnsubscribe) {
57
+ await liveUnsubscribe();
58
+ liveUnsubscribe = null;
59
+ liveRefresh = null;
60
+ }
61
+ if (destroyed)
62
+ return;
63
+ setState({ loading: true, error: null });
64
+ try {
65
+ const liveQuery = await db.live.query(sql, queryParams, handleResults);
66
+ if (destroyed) {
67
+ // Component was destroyed while we were setting up
68
+ await liveQuery.unsubscribe();
69
+ return;
70
+ }
71
+ liveUnsubscribe = liveQuery.unsubscribe;
72
+ liveRefresh = liveQuery.refresh;
73
+ // Initial results are delivered via the callback, but also available here
74
+ handleResults(liveQuery.initialResults);
75
+ }
76
+ catch (err) {
77
+ setState({
78
+ loading: false,
79
+ error: err instanceof Error ? err : new Error(String(err))
80
+ });
81
+ }
82
+ }
83
+ // Start the initial live query
84
+ setupLiveQuery(query, params);
85
+ return {
86
+ subscribe(callback) {
87
+ subscribers.add(callback);
88
+ // Immediately deliver current state (Svelte store contract)
89
+ callback(state);
90
+ return () => {
91
+ subscribers.delete(callback);
92
+ };
93
+ },
94
+ async refresh() {
95
+ if (liveRefresh) {
96
+ await liveRefresh();
97
+ }
98
+ },
99
+ async update(newQuery, newParams = []) {
100
+ await setupLiveQuery(newQuery, newParams);
101
+ },
102
+ async destroy() {
103
+ destroyed = true;
104
+ if (liveUnsubscribe) {
105
+ await liveUnsubscribe();
106
+ liveUnsubscribe = null;
107
+ liveRefresh = null;
108
+ }
109
+ subscribers.clear();
110
+ }
111
+ };
112
+ }
113
+ /**
114
+ * Convenience: create a live query store from a ParameterizedQuery object.
115
+ */
116
+ export function createLiveQueryStoreFromQuery(db, parameterizedQuery) {
117
+ return createLiveQueryStore(db, parameterizedQuery.sql, parameterizedQuery.params);
118
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Schema Introspection for GridLite
3
+ *
4
+ * Queries information_schema.columns to discover column names, types,
5
+ * and nullability for a given table. Maps Postgres data types to
6
+ * GridLite's ColumnDataType for filter operator selection and UI rendering.
7
+ */
8
+ import type { PGlite } from '@electric-sql/pglite';
9
+ import type { ColumnDataType, ColumnMetadata } from '../types.js';
10
+ /**
11
+ * Map a Postgres data_type string (from information_schema.columns)
12
+ * to a GridLite ColumnDataType.
13
+ */
14
+ export declare function mapPostgresType(postgresType: string): ColumnDataType;
15
+ /**
16
+ * Introspect a table's schema using information_schema.columns.
17
+ *
18
+ * Returns an array of ColumnMetadata in column ordinal position order.
19
+ * The table name is parameterized to prevent SQL injection.
20
+ *
21
+ * @param db - PGLite instance
22
+ * @param tableName - The table to introspect
23
+ * @param schema - The schema to search (defaults to 'public')
24
+ */
25
+ export declare function introspectTable(db: PGlite, tableName: string, schema?: string): Promise<ColumnMetadata[]>;
26
+ /**
27
+ * Get the list of column names for a table.
28
+ * Useful for the query builder's allowedColumns parameter.
29
+ */
30
+ export declare function getColumnNames(db: PGlite, tableName: string, schema?: string): Promise<string[]>;