Skip to content

feat: Phase 3 — ObjectQL Data Layer#7

Merged
hotlong merged 3 commits into
mainfrom
copilot/add-objectql-data-layer
Feb 8, 2026
Merged

feat: Phase 3 — ObjectQL Data Layer#7
hotlong merged 3 commits into
mainfrom
copilot/add-objectql-data-layer

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 8, 2026

Implements the complete Phase 3 data layer: advanced query support with filter AST, offline-first architecture with SQLite + sync queue, batch operations, and saved view storage.

Advanced Query Support

  • lib/query-builder.ts — ObjectQL filter AST types, 15 operators with field-type-aware operator mapping, serialization to wire format (['field', 'op', 'value']), compound AND/OR groups, field projections
  • hooks/useQueryBuilder.ts — Filter tree state management (add/update/remove/toggle/clear/serialize)
  • components/query/QueryBuilder, FilterRow, GlobalSearch UI
// Filter AST serialization example
const filter = createCompoundFilter("AND", [
  createSimpleFilter("status", "eq"),   // ['status', 'eq', 'active']
  createSimpleFilter("amount", "gte"),  // ['amount', 'gte', 1000]
]);
serializeFilterTree(filter); // ['AND', ['status','eq','active'], ['amount','gte',1000]]

Offline-First Architecture

  • lib/offline-storage.ts — expo-sqlite record cache with batch upsert and metadata-driven schema migration
  • lib/sync-queue.ts — Write-ahead queue with status tracking (pending → in_progress → completed/failed/conflict)
  • lib/background-sync.ts — expo-background-fetch periodic sync registration
  • hooks/useNetworkStatus.ts — Connectivity polling, updates global isOffline flag
  • hooks/useOfflineSync.ts — Auto-drains queue on reconnect, surfaces 409s as conflicts
  • components/common/OfflineIndicator.tsx, components/sync/ConflictResolutionDialog.tsx — Offline banner + conflict resolution modal (keep local vs keep server)
  • stores/sync-store.ts — Zustand store for sync state

Batch Operations

  • hooks/useBatchOperations.ts — Sequential batch create/update/delete with progress tracking and partial failure reporting
  • components/batch/BatchActionBar (multi-select actions), BatchProgressIndicator (progress bar + error summary)

View Storage

  • hooks/useViewStorage.ts — CRUD for saved views via client.views.* (accessed through typed wrapper since SDK doesn't expose types yet)
  • components/views/SaveViewDialog (name + private/shared visibility), ViewTabs (horizontal chip strip)

Dependencies

expo-sqlite, expo-network, expo-background-fetch, expo-task-manager


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits February 8, 2026 07:00
…storage, sync queue, batch ops, view storage

- lib/query-builder.ts: ObjectQL filter AST types, operators, serialization, projections
- lib/offline-storage.ts: expo-sqlite based local record cache with schema migration
- lib/sync-queue.ts: Write-ahead sync queue for offline mutations
- lib/background-sync.ts: expo-background-fetch integration for periodic sync
- stores/sync-store.ts: Zustand store for sync state
- hooks/useNetworkStatus.ts: Network connectivity detection
- hooks/useOfflineSync.ts: Auto-sync on reconnect with conflict detection
- hooks/useBatchOperations.ts: Batch create/update/delete with progress
- hooks/useQueryBuilder.ts: Filter tree management hook
- hooks/useViewStorage.ts: Saved view CRUD via client.views API
- components/query/: QueryBuilder, FilterRow, GlobalSearch UI
- components/batch/: BatchActionBar, BatchProgressIndicator UI
- components/views/: SaveViewDialog, ViewTabs UI
- components/sync/: ConflictResolutionDialog UI
- components/common/OfflineIndicator.tsx: Offline banner
- Added expo-sqlite, expo-network, expo-background-fetch, expo-task-manager deps

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Add ObjectQL data layer for Phase 3 feat: Phase 3 — ObjectQL Data Layer Feb 8, 2026
Copilot AI requested a review from hotlong February 8, 2026 07:06
@hotlong hotlong marked this pull request as ready for review February 8, 2026 07:07
Copilot AI review requested due to automatic review settings February 8, 2026 07:07
@hotlong hotlong merged commit c3e1550 into main Feb 8, 2026
2 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements Phase 3 of the ObjectQL data layer by adding query/filter AST utilities + UI, offline-first persistence and a mutation sync queue (with background task registration), batch operations helpers/UI, and saved view storage UI + hook.

Changes:

  • Added ObjectQL filter AST types/helpers (lib/query-builder.ts) and a query builder hook + UI components.
  • Added offline storage (SQLite), sync queue primitives, background fetch task registration, and Zustand sync state.
  • Added batch operations hook + UI, and saved view storage hook + UI components.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
stores/sync-store.ts Adds Zustand state for sync status, pending count, last sync time, and conflicts.
package.json Adds Expo modules for SQLite, network detection, background fetch, and task manager.
package-lock.json Locks new Expo/background-fetch/network/sqlite/task-manager dependencies.
lib/sync-queue.ts Implements SQLite-backed write-ahead mutation queue with status transitions.
lib/query-builder.ts Defines filter AST, operator metadata, type-aware operator mapping, and serialization.
lib/offline-storage.ts Implements SQLite offline cache + schema version tracking/migration markers.
lib/background-sync.ts Registers background fetch task to signal pending sync work on connectivity.
hooks/useViewStorage.ts Adds hook to list/create/update/delete saved views via client.views.*.
hooks/useQueryBuilder.ts Adds hook for composing filter trees + projections.
hooks/useOfflineSync.ts Adds foreground sync loop that drains the queue when online.
hooks/useNetworkStatus.ts Adds polling-based network state hook updating global offline flag.
hooks/useBatchOperations.ts Adds sequential batch create/update/delete with progress + error reporting.
docs/ROADMAP.md Marks Phase 3 roadmap items as completed.
components/views/index.ts Barrel exports for views components.
components/views/ViewTabs.tsx Adds horizontal saved-view tab strip UI.
components/views/SaveViewDialog.tsx Adds modal UI to save/edit a view name + visibility.
components/sync/index.ts Barrel exports for sync components.
components/sync/ConflictResolutionDialog.tsx Adds modal UI for resolving sync conflicts (keep local/server).
components/query/index.ts Barrel exports for query builder components.
components/query/QueryBuilder.tsx Adds query builder container UI (root logic toggle + rows).
components/query/GlobalSearch.tsx Adds global search input UI.
components/query/FilterRow.tsx Adds per-filter row UI (field/operator/value inputs).
components/common/OfflineIndicator.tsx Adds offline/pending-changes banner with optional manual sync action.
components/batch/index.ts Barrel exports for batch components.
components/batch/BatchProgressIndicator.tsx Adds progress + result summary UI for batch operations.
components/batch/BatchActionBar.tsx Adds bottom action bar UI for multi-select bulk actions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/sync-queue.ts
Comment on lines +110 to +114
/** Get all pending entries, oldest first */
export function getPendingEntries(): SyncQueueEntry[] {
const db = getDatabase();
const rows = db.getAllSync<Record<string, unknown>>(
"SELECT * FROM sync_queue WHERE status = 'pending' ORDER BY created_at ASC",
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getPendingEntries() only selects rows with status = 'pending', but getPendingCount() counts both pending and failed, and markFailed() moves entries to failed. As written, failed entries will never be retried by the sync hook unless something calls resetEntry(). Consider including failed in getPendingEntries() (or adding explicit retry handling) so failures can be retried or surfaced predictably.

Suggested change
/** Get all pending entries, oldest first */
export function getPendingEntries(): SyncQueueEntry[] {
const db = getDatabase();
const rows = db.getAllSync<Record<string, unknown>>(
"SELECT * FROM sync_queue WHERE status = 'pending' ORDER BY created_at ASC",
/** Get all pending entries (including failed), oldest first */
export function getPendingEntries(): SyncQueueEntry[] {
const db = getDatabase();
const rows = db.getAllSync<Record<string, unknown>>(
"SELECT * FROM sync_queue WHERE status IN ('pending','failed') ORDER BY created_at ASC",

Copilot uses AI. Check for mistakes.
Comment thread lib/sync-queue.ts
Comment on lines +151 to +155
const db = getDatabase();
db.runSync(
"UPDATE sync_queue SET status = 'in_progress', updated_at = ? WHERE id = ?",
[Date.now(), id],
);
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entries marked in_progress are never selected by getPendingEntries(), and there’s no helper to recover/reset in_progress rows after an app crash/force-quit. That can permanently strand work items. Consider resetting any in_progress entries back to pending on startup (or treating in_progress as eligible after a timeout).

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +50
// Cycle through fields for simplicity
const currentIdx = fields.findIndex((f) => f.name === filter.field);
const nextField = fields[(currentIdx + 1) % fields.length];
if (nextField) {
onUpdate({ field: nextField.name });
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If fields is empty, (currentIdx + 1) % fields.length evaluates to NaN, so nextField becomes undefined. Guard against fields.length === 0 before doing the modulo/cycle logic to avoid runtime issues when metadata hasn’t loaded yet.

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +72
break;
case "update":
if (item.recordId) {
await client.data.update(objectName, item.recordId, item.data ?? {});
}
break;
case "delete":
if (item.recordId) {
await client.data.delete(objectName, item.recordId);
}
break;
}
result.succeeded++;
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

result.succeeded++ is executed even when an update/delete item has no recordId and therefore no server call was made. If missing recordId should be an error, move the success increment inside the branches that actually perform the operation.

Suggested change
break;
case "update":
if (item.recordId) {
await client.data.update(objectName, item.recordId, item.data ?? {});
}
break;
case "delete":
if (item.recordId) {
await client.data.delete(objectName, item.recordId);
}
break;
}
result.succeeded++;
result.succeeded++;
break;
case "update":
if (item.recordId) {
await client.data.update(objectName, item.recordId, item.data ?? {});
result.succeeded++;
}
break;
case "delete":
if (item.recordId) {
await client.data.delete(objectName, item.recordId);
result.succeeded++;
}
break;
}

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +13
isCompoundFilter,
isSimpleFilter,
OPERATOR_META,
operatorsForFieldType,
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several symbols imported from ~/lib/query-builder appear unused in this file (e.g., isCompoundFilter, OPERATOR_META, operatorsForFieldType, etc.). With the current ESLint config this will fail linting; remove unused imports or use them in rendering logic.

Suggested change
isCompoundFilter,
isSimpleFilter,
OPERATOR_META,
operatorsForFieldType,
isSimpleFilter,

Copilot uses AI. Check for mistakes.
Comment thread lib/background-sync.ts
Comment on lines +4 to +5
* Registers a periodic background task that drains the offline sync queue
* when the device regains connectivity.
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module header says the background task “drains the offline sync queue”, but the task currently only checks connectivity/queue length and returns NewData without processing entries. Either update this docstring to match the signal-only behavior, or implement real draining here if that’s the intent.

Suggested change
* Registers a periodic background task that drains the offline sync queue
* when the device regains connectivity.
* Registers a periodic background task that checks connectivity and the
* offline sync queue, and signals that pending work should be processed
* by the foreground sync logic when the app is next active.

Copilot uses AI. Check for mistakes.
Comment thread lib/background-sync.ts

/**
* The task body executed in the background.
* It checks connectivity and processes pending queue entries.
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment says the task “processes pending queue entries”, but no entries are actually processed/marked—only counted. Consider revising the comment to reflect the current behavior, or add the missing processing logic.

Suggested change
* It checks connectivity and processes pending queue entries.
* It checks connectivity and inspects pending queue entries to signal work
* for the foreground sync logic.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +33
const [name, setName] = useState(initialValues?.name ?? "");
const [visibility, setVisibility] = useState<"private" | "shared">(
initialValues?.visibility ?? "private",
);
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name/visibility state is initialized from initialValues only on the first render. When the dialog is reopened (or initialValues changes), the inputs will retain the previous state instead of reflecting the new values. Consider syncing state in an effect keyed on visible/initialValues (or resetting state on close) so editing different views shows correct defaults.

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +69
if (item.recordId) {
await client.data.delete(objectName, item.recordId);
}
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the delete case, if item.recordId is missing the code skips the API call without recording a failure. Consider treating missing recordId as an error for delete items so the batch result matches actual work performed.

Suggested change
if (item.recordId) {
await client.data.delete(objectName, item.recordId);
}
if (!item.recordId) {
throw new Error("Missing recordId for delete operation");
}
await client.data.delete(objectName, item.recordId);

Copilot uses AI. Check for mistakes.
import type { FieldDefinition } from "~/components/renderers/types";
import {
type SimpleFilter,
type FilterOperator,
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FilterOperator is imported but never used in this file. Remove the unused import to satisfy linting.

Suggested change
type FilterOperator,

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants