supastash 0.2.14 → 0.2.16

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.
@@ -1 +1 @@
1
- {"version":3,"file":"syncEngine.d.ts","sourceRoot":"","sources":["../../../src/desktop/hooks/syncEngine.ts"],"names":[],"mappings":"AA6BA;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAqCnE;AA2DD;;;;;GAKG;AACH,wBAAgB,aAAa;;;EA2D5B;AAMD;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgC5D;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnD"}
1
+ {"version":3,"file":"syncEngine.d.ts","sourceRoot":"","sources":["../../../src/desktop/hooks/syncEngine.ts"],"names":[],"mappings":"AA8BA;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAqCnE;AAyDD;;;;;GAKG;AACH,wBAAgB,aAAa;;;EA2D5B;AAMD;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoC5D;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnD"}
@@ -9,6 +9,7 @@ import { subscribeToAppVisibility } from "../adapters/appstate";
9
9
  import { pullFromRemote as doPullFromRemote } from "../utils/sync/pullFromRemote";
10
10
  import { pullFromRemoteBatch } from "../utils/sync/pullFromRemote/pullFromRemoteBatch";
11
11
  import { updateLocalDb } from "../utils/sync/pullFromRemote/updateLocalDb";
12
+ import { pushLocalData as doPushLocalData } from "../utils/sync/pushLocal";
12
13
  import { pushLocalDataToRemote } from "../utils/sync/pushLocal/sendUnsyncedToSupabase";
13
14
  // -----------------------------
14
15
  // Module-scoped state & tunables
@@ -85,12 +86,7 @@ async function pushLocalDataSafe() {
85
86
  return;
86
87
  isPushing = true;
87
88
  try {
88
- if (cfg.useBatchPullSync) {
89
- await pullFromRemoteBatch();
90
- }
91
- else {
92
- await doPullFromRemote();
93
- }
89
+ await doPushLocalData();
94
90
  lastPushAt = Date.now();
95
91
  }
96
92
  catch (e) {
@@ -108,7 +104,6 @@ async function pullFromRemoteSafe() {
108
104
  return;
109
105
  if (!(await isOnline()))
110
106
  return;
111
- // If in ghost mode, don't pull
112
107
  const cfg = getSupastashConfig();
113
108
  if (cfg.supastashMode === "ghost")
114
109
  return;
@@ -116,7 +111,12 @@ async function pullFromRemoteSafe() {
116
111
  return;
117
112
  isPulling = true;
118
113
  try {
119
- await doPullFromRemote();
114
+ if (cfg.useBatchPullSync) {
115
+ await pullFromRemoteBatch();
116
+ }
117
+ else {
118
+ await doPullFromRemote();
119
+ }
120
120
  lastPullAt = Date.now();
121
121
  }
122
122
  catch (e) {
@@ -206,7 +206,12 @@ export async function syncTable(table) {
206
206
  // Pull
207
207
  if (cfg.syncEngine?.pull) {
208
208
  try {
209
- await updateLocalDb(table, filter, syncCalls.get(table)?.pull);
209
+ if (cfg.useBatchPullSync) {
210
+ await pullFromRemoteBatch([table]);
211
+ }
212
+ else {
213
+ await updateLocalDb(table, filter, syncCalls.get(table)?.pull);
214
+ }
210
215
  }
211
216
  catch (e) {
212
217
  log("[Supastash] syncTable pull error", {
@@ -5,5 +5,5 @@
5
5
  * Requires `useBatchPullSync: true` in config and the
6
6
  * `supastash_pull_sync` Postgres function to be deployed.
7
7
  */
8
- export declare function pullFromRemoteBatch(): Promise<void>;
8
+ export declare function pullFromRemoteBatch(specificTables?: string[]): Promise<void>;
9
9
  //# sourceMappingURL=pullFromRemoteBatch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pullFromRemoteBatch.d.ts","sourceRoot":"","sources":["../../../../../src/desktop/utils/sync/pullFromRemote/pullFromRemoteBatch.ts"],"names":[],"mappings":"AA+CA;;;;;;GAMG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CA4MzD"}
1
+ {"version":3,"file":"pullFromRemoteBatch.d.ts","sourceRoot":"","sources":["../../../../../src/desktop/utils/sync/pullFromRemote/pullFromRemoteBatch.ts"],"names":[],"mappings":"AA+CA;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,cAAc,CAAC,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,IAAI,CAAC,CA4Mf"}
@@ -37,14 +37,14 @@ function buildCursorFilter(tsCol, lastSyncedAt, lastPk) {
37
37
  * Requires `useBatchPullSync: true` in config and the
38
38
  * `supastash_pull_sync` Postgres function to be deployed.
39
39
  */
40
- export async function pullFromRemoteBatch() {
40
+ export async function pullFromRemoteBatch(specificTables) {
41
41
  const cfg = getSupastashConfig();
42
42
  const supabase = cfg.supabaseClient;
43
43
  if (!supabase)
44
44
  throw new Error(`No supabase client found: ${supabaseClientErr}`);
45
45
  if (cfg.supastashMode === "ghost")
46
46
  return;
47
- const tables = await getAllTables();
47
+ const tables = specificTables ?? (await getAllTables());
48
48
  if (!tables) {
49
49
  log("[Supastash] Batch pull: no tables found");
50
50
  return;
@@ -129,7 +129,7 @@ export async function upsertData({ tx, table, record, doesExist, }) {
129
129
  try {
130
130
  const db = tx ?? (await getSupastashDb());
131
131
  const allColumns = await getTableSchema(table);
132
- const pullFilterColumns = new Set(cfg.filterColumns?.pull?.[table] ?? []);
132
+ const pullFilterColumns = new Set(cfg.ignoredColumns?.pull?.[table] ?? []);
133
133
  const columns = pullFilterColumns.size
134
134
  ? allColumns.filter((c) => !pullFilterColumns.has(c))
135
135
  : allColumns;
@@ -200,7 +200,7 @@ export async function upsertChunkData({ tx, table, records, }) {
200
200
  return;
201
201
  const db = tx ?? (await getSupastashDb());
202
202
  const allColumns = await getTableSchema(table);
203
- const pullFilterColumns = new Set(cfg.filterColumns?.pull?.[table] ?? []);
203
+ const pullFilterColumns = new Set(cfg.ignoredColumns?.pull?.[table] ?? []);
204
204
  const columns = pullFilterColumns.size
205
205
  ? allColumns.filter((c) => !pullFilterColumns.has(c))
206
206
  : allColumns;
@@ -204,7 +204,7 @@ export async function uploadData(table, unsyncedRecords, onPushToRemote) {
204
204
  const supabase = cfg.supabaseClient;
205
205
  if (!supabase)
206
206
  throw new Error("[Supastash] Supabase client not configured");
207
- const pushFilterColumns = cfg.filterColumns?.push?.[table] ?? [];
207
+ const pushFilterColumns = cfg.ignoredColumns?.push?.[table] ?? [];
208
208
  const cleaned = unsyncedRecords.map(({ synced_at, deleted_at, arrived_at, ...rest }) => {
209
209
  const row = enforceTimestamps(normalizeForSupabase(rest));
210
210
  for (const col of pushFilterColumns)
@@ -1 +1 @@
1
- {"version":3,"file":"syncEngine.d.ts","sourceRoot":"","sources":["../../../src/native/hooks/syncEngine.ts"],"names":[],"mappings":"AA8BA;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAqCnE;AAyDD;;;;;GAKG;AACH,wBAAgB,aAAa;;;EA+D5B;AAMD;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgC5D;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnD"}
1
+ {"version":3,"file":"syncEngine.d.ts","sourceRoot":"","sources":["../../../src/native/hooks/syncEngine.ts"],"names":[],"mappings":"AA8BA;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,KAAK,GAAE,OAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAqCnE;AAyDD;;;;;GAKG;AACH,wBAAgB,aAAa;;;EA+D5B;AAMD;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoC5D;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnD"}
@@ -208,7 +208,12 @@ export async function syncTable(table) {
208
208
  // Pull
209
209
  if (cfg.syncEngine?.pull) {
210
210
  try {
211
- await updateLocalDb(table, filter, syncCalls.get(table)?.pull);
211
+ if (cfg.useBatchPullSync) {
212
+ await pullFromRemoteBatch([table]);
213
+ }
214
+ else {
215
+ await updateLocalDb(table, filter, syncCalls.get(table)?.pull);
216
+ }
212
217
  }
213
218
  catch (e) {
214
219
  log("[Supastash] syncTable pull error", {
@@ -5,5 +5,5 @@
5
5
  * Requires `useBatchPullSync: true` in config and the
6
6
  * `supastash_pull_sync` Postgres function to be deployed.
7
7
  */
8
- export declare function pullFromRemoteBatch(): Promise<void>;
8
+ export declare function pullFromRemoteBatch(specificTables?: string[]): Promise<void>;
9
9
  //# sourceMappingURL=pullFromRemoteBatch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pullFromRemoteBatch.d.ts","sourceRoot":"","sources":["../../../../../src/native/utils/sync/pullFromRemote/pullFromRemoteBatch.ts"],"names":[],"mappings":"AA4CA;;;;;;GAMG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CA+KzD"}
1
+ {"version":3,"file":"pullFromRemoteBatch.d.ts","sourceRoot":"","sources":["../../../../../src/native/utils/sync/pullFromRemote/pullFromRemoteBatch.ts"],"names":[],"mappings":"AA+CA;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,cAAc,CAAC,EAAE,MAAM,EAAE,GACxB,OAAO,CAAC,IAAI,CAAC,CA4Mf"}
@@ -2,16 +2,16 @@ import { getSupastashConfig } from "../../../../shared/core/config";
2
2
  import { getSupastashDb } from "../../../../shared/db/dbInitializer";
3
3
  import { rpcTableFilters } from "../../../../shared/store/rpcTableFilters";
4
4
  import { tableFilters } from "../../../../shared/store/tableFilters";
5
- import { postgrestFiltersToRpc } from "../../../../shared/utils/sync/pullFromRemote/postgrestToRpc";
5
+ import log, { logError, logWarn } from "../../../../shared/utils/logs";
6
+ import { refreshScreen } from "../../../../shared/utils/refreshScreenCalls";
7
+ import { supabaseClientErr } from "../../../../shared/utils/supabaseClientErr";
6
8
  import { getAllTables } from "../../../../shared/utils/sync/getAllTables";
7
9
  import { getMaxSyncLookBack, logNoUpdates, returnMaxDate, } from "../../../../shared/utils/sync/pullFromRemote/helpers";
10
+ import { postgrestFiltersToRpc } from "../../../../shared/utils/sync/pullFromRemote/postgrestToRpc";
8
11
  import { SyncInfoUpdater } from "../../../../shared/utils/sync/queryStatus";
9
- import { refreshScreen } from "../../../../shared/utils/refreshScreenCalls";
10
- import log, { logError, logWarn } from "../../../../shared/utils/logs";
11
- import { supabaseClientErr } from "../../../../shared/utils/supabaseClientErr";
12
12
  import { prefetchRemoteTableSchemas } from "../../../../shared/utils/sync/status/remoteSchema";
13
- import { setSupastashSyncStatus } from "../status/services";
14
13
  import { selectSyncStatus } from "../status/repo";
14
+ import { setSupastashSyncStatus } from "../status/services";
15
15
  import { upsertChunkData } from "./updateLocalDb";
16
16
  const CHUNK_SIZE = 999;
17
17
  function buildCursorFilter(tsCol, lastSyncedAt, lastPk) {
@@ -37,14 +37,14 @@ function buildCursorFilter(tsCol, lastSyncedAt, lastPk) {
37
37
  * Requires `useBatchPullSync: true` in config and the
38
38
  * `supastash_pull_sync` Postgres function to be deployed.
39
39
  */
40
- export async function pullFromRemoteBatch() {
40
+ export async function pullFromRemoteBatch(specificTables) {
41
41
  const cfg = getSupastashConfig();
42
42
  const supabase = cfg.supabaseClient;
43
43
  if (!supabase)
44
44
  throw new Error(`No supabase client found: ${supabaseClientErr}`);
45
45
  if (cfg.supastashMode === "ghost")
46
46
  return;
47
- const tables = await getAllTables();
47
+ const tables = specificTables ?? (await getAllTables());
48
48
  if (!tables) {
49
49
  log("[Supastash] Batch pull: no tables found");
50
50
  return;
@@ -57,7 +57,10 @@ export async function pullFromRemoteBatch() {
57
57
  const db = await getSupastashDb();
58
58
  const completedTables = new Set();
59
59
  SyncInfoUpdater.setInProgress({ action: "start", type: "pull" });
60
- SyncInfoUpdater.setNumberOfTables({ amount: tablesToPull.length, type: "pull" });
60
+ SyncInfoUpdater.setNumberOfTables({
61
+ amount: tablesToPull.length,
62
+ type: "pull",
63
+ });
61
64
  // Warm schema cache for all tables in one call if enabled
62
65
  if (cfg.useBatchSchemaFetch) {
63
66
  await prefetchRemoteTableSchemas(tablesToPull);
@@ -112,8 +115,16 @@ export async function pullFromRemoteBatch() {
112
115
  logWarn(`[Supastash] Batch: skipped row without id from "${table}"`);
113
116
  continue;
114
117
  }
115
- prevMaxSyncedAt = returnMaxDate({ row, prevMax: prevMaxSyncedAt, col: tsCol });
116
- prevMaxDeletedAt = returnMaxDate({ row, prevMax: prevMaxDeletedAt, col: "deleted_at" });
118
+ prevMaxSyncedAt = returnMaxDate({
119
+ row,
120
+ prevMax: prevMaxSyncedAt,
121
+ col: tsCol,
122
+ });
123
+ prevMaxDeletedAt = returnMaxDate({
124
+ row,
125
+ prevMax: prevMaxDeletedAt,
126
+ col: "deleted_at",
127
+ });
117
128
  if (row.deleted_at) {
118
129
  toDelete.push(row.id);
119
130
  }
@@ -121,8 +132,16 @@ export async function pullFromRemoteBatch() {
121
132
  toUpsert.push(row);
122
133
  }
123
134
  }
124
- SyncInfoUpdater.setUnsyncedDataCount({ amount: toUpsert.length, type: "pull", table });
125
- SyncInfoUpdater.setUnsyncedDeletedCount({ amount: toDelete.length, type: "pull", table });
135
+ SyncInfoUpdater.setUnsyncedDataCount({
136
+ amount: toUpsert.length,
137
+ type: "pull",
138
+ table,
139
+ });
140
+ SyncInfoUpdater.setUnsyncedDeletedCount({
141
+ amount: toDelete.length,
142
+ type: "pull",
143
+ table,
144
+ });
126
145
  // Delete soft-deleted rows
127
146
  if (toDelete.length > 0) {
128
147
  for (let i = 0; i < toDelete.length; i += CHUNK_SIZE) {
@@ -128,7 +128,7 @@ export async function upsertData({ tx, table, record, doesExist, }) {
128
128
  try {
129
129
  const db = tx ?? (await getSupastashDb());
130
130
  const allColumns = await getTableSchema(table);
131
- const pullFilterColumns = new Set(cfg.filterColumns?.pull?.[table] ?? []);
131
+ const pullFilterColumns = new Set(cfg.ignoredColumns?.pull?.[table] ?? []);
132
132
  const columns = pullFilterColumns.size
133
133
  ? allColumns.filter((c) => !pullFilterColumns.has(c))
134
134
  : allColumns;
@@ -199,7 +199,7 @@ export async function upsertChunkData({ tx, table, records, }) {
199
199
  return;
200
200
  const db = tx ?? (await getSupastashDb());
201
201
  const allColumns = await getTableSchema(table);
202
- const pullFilterColumns = new Set(cfg.filterColumns?.pull?.[table] ?? []);
202
+ const pullFilterColumns = new Set(cfg.ignoredColumns?.pull?.[table] ?? []);
203
203
  const columns = pullFilterColumns.size
204
204
  ? allColumns.filter((c) => !pullFilterColumns.has(c))
205
205
  : allColumns;
@@ -207,7 +207,7 @@ export async function uploadData(table, unsyncedRecords, onPushToRemote) {
207
207
  const supabase = cfg.supabaseClient;
208
208
  if (!supabase)
209
209
  throw new Error("[Supastash] Supabase client not configured");
210
- const pushFilterColumns = cfg.filterColumns?.push?.[table] ?? [];
210
+ const pushFilterColumns = cfg.ignoredColumns?.push?.[table] ?? [];
211
211
  const cleaned = unsyncedRecords.map(({ synced_at, deleted_at, arrived_at, ...rest }) => {
212
212
  const row = enforceTimestamps(normalizeForSupabase(rest));
213
213
  for (const col of pushFilterColumns)
@@ -30,7 +30,7 @@ let _config = {
30
30
  deleteConflictedRows: false,
31
31
  pushRPCPath: undefined,
32
32
  supastashMode: "live",
33
- filterColumns: {
33
+ ignoredColumns: {
34
34
  push: {},
35
35
  pull: {},
36
36
  },
@@ -118,9 +118,9 @@ export function configureSupastash(config) {
118
118
  ..._config.fieldEnforcement,
119
119
  ...config.fieldEnforcement,
120
120
  },
121
- filterColumns: {
122
- push: config.filterColumns?.push ?? _config.filterColumns?.push ?? {},
123
- pull: config.filterColumns?.pull ?? _config.filterColumns?.pull ?? {},
121
+ ignoredColumns: {
122
+ push: config.ignoredColumns?.push ?? _config.ignoredColumns?.push ?? {},
123
+ pull: config.ignoredColumns?.pull ?? _config.ignoredColumns?.pull ?? {},
124
124
  },
125
125
  };
126
126
  _configured = true;
@@ -154,12 +154,12 @@ export type SupastashConfig<T extends SupastashSQLiteClientTypes> = {
154
154
  * - `pull`: columns removed from each row before it is written to the local SQLite DB.
155
155
  *
156
156
  * @example
157
- * filterColumns: {
157
+ * ignoredColumns: {
158
158
  * push: { orders: ["internal_notes", "cost_price"] },
159
159
  * pull: { users: ["password_hash", "secret_token"] },
160
160
  * }
161
161
  */
162
- filterColumns?: {
162
+ ignoredColumns?: {
163
163
  push?: Record<string, string[]>;
164
164
  pull?: Record<string, string[]>;
165
165
  };
@@ -1,4 +1,13 @@
1
1
  import { PayloadData } from "../../../types/query.types";
2
+ import { SupastashFilter } from "../../../types/realtimeData.types";
3
+ /**
4
+ * Builds a combined " AND (...)" SQL string + params array from a list of filters.
5
+ * Never throws — malformed filters are skipped and logged.
6
+ */
7
+ export declare function buildFilterSql(filters: SupastashFilter[]): {
8
+ sql: string;
9
+ params: unknown[];
10
+ };
2
11
  /**
3
12
  * Gets all unsynced data from a table
4
13
  * @param table - The table to get the data from
@@ -1 +1 @@
1
- {"version":3,"file":"getAllUnsyncedData.d.ts","sourceRoot":"","sources":["../../../../../src/shared/utils/sync/pushLocal/getAllUnsyncedData.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAwCzD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,CAe/B;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,CAM/B"}
1
+ {"version":3,"file":"getAllUnsyncedData.d.ts","sourceRoot":"","sources":["../../../../../src/shared/utils/sync/pushLocal/getAllUnsyncedData.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAgGpE;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG;IAC1D,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,OAAO,EAAE,CAAC;CACnB,CAYA;AAmCD;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,CAuB/B;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,CAS/B"}
@@ -1,8 +1,99 @@
1
1
  import { getSupastashDb } from "../../../db/dbInitializer";
2
+ import { tableFilters } from "../../../store/tableFilters";
2
3
  import { getTableSchema } from "../../getTableSchema";
3
4
  import log from "../../logs";
4
5
  import { getRemoteTableSchema } from "../status/remoteSchema";
5
6
  const sharedKeysCache = new Map();
7
+ const VALID_OPERATORS = new Set([
8
+ "eq",
9
+ "neq",
10
+ "gt",
11
+ "gte",
12
+ "lt",
13
+ "lte",
14
+ "is",
15
+ "in",
16
+ ]);
17
+ function escapeColumn(column) {
18
+ // Basic guard against injection via column names (should only ever be internal identifiers)
19
+ return `"${column.replace(/"/g, '""')}"`;
20
+ }
21
+ /**
22
+ * Builds a single SQL clause + params for one filter condition.
23
+ * Returns null (instead of throwing) if the filter is malformed.
24
+ */
25
+ function buildSingleClause(f) {
26
+ try {
27
+ // Composite OR filter: { or: SupastashFilter[] }
28
+ if ("or" in f && Array.isArray(f.or)) {
29
+ const orFilters = f.or;
30
+ const built = orFilters
31
+ .map((sub) => buildSingleClause(sub))
32
+ .filter((c) => !!c);
33
+ if (!built.length)
34
+ return null;
35
+ const sql = "(" + built.map((c) => c.sql).join(" OR ") + ")";
36
+ const params = built.flatMap((c) => c.params);
37
+ return { sql, params };
38
+ }
39
+ // Flat filter: { column, operator, value }
40
+ const { column, operator, value } = f;
41
+ if (!column || !operator || !VALID_OPERATORS.has(operator)) {
42
+ log(`[Supastash] Skipping invalid filter: ${JSON.stringify(f)}`);
43
+ return null;
44
+ }
45
+ const col = escapeColumn(column);
46
+ switch (operator) {
47
+ case "is":
48
+ // sqlite uses IS for null comparisons; also support IS TRUE/FALSE-ish via 0/1
49
+ if (value === null) {
50
+ return { sql: `${col} IS NULL`, params: [] };
51
+ }
52
+ return { sql: `${col} IS ?`, params: [value] };
53
+ case "in": {
54
+ const values = Array.isArray(value) ? value : [value];
55
+ if (!values.length)
56
+ return null;
57
+ const placeholders = values.map(() => "?").join(", ");
58
+ return { sql: `${col} IN (${placeholders})`, params: values };
59
+ }
60
+ case "eq":
61
+ return { sql: `${col} = ?`, params: [value] };
62
+ case "neq":
63
+ return { sql: `${col} != ?`, params: [value] };
64
+ case "gt":
65
+ return { sql: `${col} > ?`, params: [value] };
66
+ case "gte":
67
+ return { sql: `${col} >= ?`, params: [value] };
68
+ case "lt":
69
+ return { sql: `${col} < ?`, params: [value] };
70
+ case "lte":
71
+ return { sql: `${col} <= ?`, params: [value] };
72
+ default:
73
+ return null;
74
+ }
75
+ }
76
+ catch (err) {
77
+ log(`[Supastash] Failed to build filter clause: ${String(err)}`);
78
+ return null;
79
+ }
80
+ }
81
+ /**
82
+ * Builds a combined " AND (...)" SQL string + params array from a list of filters.
83
+ * Never throws — malformed filters are skipped and logged.
84
+ */
85
+ export function buildFilterSql(filters) {
86
+ if (!filters?.length)
87
+ return { sql: "", params: [] };
88
+ const clauses = filters
89
+ .map((f) => buildSingleClause(f))
90
+ .filter((c) => !!c);
91
+ if (!clauses.length)
92
+ return { sql: "", params: [] };
93
+ const sql = " AND " + clauses.map((c) => c.sql).join(" AND ");
94
+ const params = clauses.flatMap((c) => c.params);
95
+ return { sql, params };
96
+ }
6
97
  async function getRemoteKeys(table) {
7
98
  if (sharedKeysCache.has(table)) {
8
99
  return sharedKeysCache.get(table);
@@ -30,15 +121,17 @@ async function getRemoteKeys(table) {
30
121
  */
31
122
  export async function getAllUnsyncedData(table) {
32
123
  const db = await getSupastashDb();
124
+ const filters = tableFilters.get(table) ?? [];
125
+ const filterSqlDetails = buildFilterSql(filters);
33
126
  const remoteKeys = await getRemoteKeys(table);
34
127
  if (!remoteKeys) {
35
- const query = `SELECT * FROM ${table} WHERE synced_at IS NULL AND deleted_at IS NULL`;
36
- const data = await db.getAllAsync(query);
128
+ const query = `SELECT * FROM ${table} WHERE synced_at IS NULL AND deleted_at IS NULL${filterSqlDetails.sql}`;
129
+ const data = await db.getAllAsync(query, filterSqlDetails.params);
37
130
  return data ?? null;
38
131
  }
39
132
  const columns = remoteKeys.join(", ");
40
- const query = `SELECT ${columns} FROM ${table} WHERE synced_at IS NULL AND deleted_at IS NULL`;
41
- const data = await db.getAllAsync(query);
133
+ const query = `SELECT ${columns} FROM ${table} WHERE synced_at IS NULL AND deleted_at IS NULL${filterSqlDetails.sql}`;
134
+ const data = await db.getAllAsync(query, filterSqlDetails.params);
42
135
  return data ?? null;
43
136
  }
44
137
  /**
@@ -48,6 +141,8 @@ export async function getAllUnsyncedData(table) {
48
141
  */
49
142
  export async function getAllDeletedData(table) {
50
143
  const db = await getSupastashDb();
51
- const data = await db.getAllAsync(`SELECT deleted_at, id FROM ${table} WHERE deleted_at IS NOT NULL`);
144
+ const filters = tableFilters.get(table) ?? [];
145
+ const filterSqlDetails = buildFilterSql(filters);
146
+ const data = await db.getAllAsync(`SELECT deleted_at, id FROM ${table} WHERE deleted_at IS NOT NULL${filterSqlDetails.sql}`, filterSqlDetails.params);
52
147
  return data ?? null;
53
148
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supastash",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "type": "module",