@appurist/offlinedb 1.0.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/docs/API.md ADDED
@@ -0,0 +1,595 @@
1
+ # API Reference
2
+
3
+ This document describes the current public API exported by `@appurist/offlinedb`.
4
+
5
+ Current exports from [src/index.js](C:/dev/appurist/offline/offlinedb/src/index.js):
6
+
7
+ - `OfflineDbClient`
8
+ - `IndexedDbPersistence`
9
+ - `InMemoryPersistence`
10
+ - `LocalStoragePersistence`
11
+ - `createNeonHttpTransport`
12
+ - `createOdbAuthClient`
13
+ - `createOdbDataClient`
14
+ - `createOdbSyncTransport`
15
+ - `defineTable`
16
+ - `createOfflinedbInstallSql`
17
+ - `createSyncedTableSql`
18
+
19
+ All Neon URLs used by these APIs are developer-supplied configuration.
20
+ The library does not embed project-specific Neon Auth or Data API endpoints.
21
+
22
+ ## `OfflineDbClient`
23
+
24
+ Browser-first offline replication client.
25
+
26
+ ### `OfflineDbClient.open(options)`
27
+
28
+ Initializes the provided persistence layer and returns a client instance.
29
+ If `persistence` is omitted, `open(...)` uses the `"indexeddb"` built-in automatically.
30
+
31
+ ```js
32
+ const client = await OfflineDbClient.open({
33
+ persistence: "indexeddb",
34
+ databaseName: "app-cache",
35
+ transport,
36
+ tables
37
+ });
38
+ ```
39
+
40
+ #### `options`
41
+
42
+ - `persistence`
43
+ Optional persistence selector or persistence object.
44
+ Supported built-in selectors are:
45
+ - `"indexeddb"`
46
+ - `"localstorage"`
47
+ - `"memory"`
48
+ You may also pass an object implementing the persistence contract documented below.
49
+ - `databaseName`
50
+ Optional database/storage namespace used by the built-in persistence implementations.
51
+ Defaults to `offlinedb`.
52
+ - `indexedDb`
53
+ Optional IndexedDB factory override used by the `"indexeddb"` built-in.
54
+ - `localStorage`
55
+ Optional localStorage override used by the `"localstorage"` built-in.
56
+ - `transport`
57
+ Object with a `sync(request)` method returning a sync result.
58
+ - `tables`
59
+ Array of table definitions created by `defineTable(...)`.
60
+ - `now`
61
+ Optional clock function returning a `Date`.
62
+ Used when creating optimistic local writes.
63
+ - `idFactory`
64
+ Optional local-id generator.
65
+ Defaults to `crypto.randomUUID()` when available.
66
+
67
+ ### Instance methods
68
+
69
+ #### `await client.get(table, key)`
70
+
71
+ Returns one locally stored row or `null`.
72
+
73
+ ```js
74
+ const task = await client.get("tasks", "task-1");
75
+ ```
76
+
77
+ - `table`
78
+ Registered table name.
79
+ - `key`
80
+ Primary key value as a string.
81
+
82
+ #### `await client.list(table)`
83
+
84
+ Returns all locally stored rows for one registered table.
85
+
86
+ ```js
87
+ const tasks = await client.list("tasks");
88
+ ```
89
+
90
+ #### `await client.mutate(args)`
91
+
92
+ Creates an optimistic local change, applies it to the local replica, and appends it to the pending queue.
93
+
94
+ ```js
95
+ const mutation = await client.mutate({
96
+ table: "tasks",
97
+ key: "task-1",
98
+ values: {
99
+ owner_id: "user-1",
100
+ title: "Write docs"
101
+ },
102
+ metadata: {
103
+ source: "ui"
104
+ }
105
+ });
106
+ ```
107
+
108
+ ##### `args`
109
+
110
+ - `table`
111
+ Registered table name.
112
+ - `key`
113
+ Primary key value for the target row.
114
+ - `values`
115
+ Full or partial row object to merge into the current local row.
116
+ The client also injects:
117
+ - the table primary key field
118
+ - the next optimistic revision
119
+ - the updated-at column
120
+ - the new local id
121
+ - `global_id = 0`
122
+ - `metadata`
123
+ Optional metadata preserved in the queued change object.
124
+
125
+ ##### Return value
126
+
127
+ Returns a pending change object with:
128
+
129
+ - `localId`
130
+ - `globalId`
131
+ - `clientSequence`
132
+ - `table`
133
+ - `key`
134
+ - `baseRevision`
135
+ - `values`
136
+ - `occurredAt`
137
+ - `metadata`
138
+
139
+ #### `await client.sync(options?)`
140
+
141
+ Pushes pending local changes and pulls remote changes through the configured transport.
142
+
143
+ ```js
144
+ const result = await client.sync();
145
+ ```
146
+
147
+ ##### `options`
148
+
149
+ - `tables`
150
+ Optional list of registered table names to include in this sync pass.
151
+ When omitted, the client syncs all registered tables.
152
+
153
+ ##### Request sent to transport
154
+
155
+ The client calls:
156
+
157
+ ```js
158
+ await transport.sync({
159
+ mutations,
160
+ tables,
161
+ lastGlobalId
162
+ });
163
+ ```
164
+
165
+ Where:
166
+
167
+ - `mutations`
168
+ The current pending local-change queue, sorted by `clientSequence`.
169
+ - `tables`
170
+ Optional table-name filter for the current sync pass.
171
+ - `lastGlobalId`
172
+ The client's last applied global id.
173
+
174
+ ##### Expected transport response
175
+
176
+ The transport must return:
177
+
178
+ ```js
179
+ {
180
+ accepted: [
181
+ {
182
+ localId: "m1",
183
+ globalId: 12
184
+ }
185
+ ],
186
+ conflicts: [
187
+ {
188
+ localId: "m2",
189
+ code: "revision_conflict",
190
+ serverRow: { ... } // or null
191
+ }
192
+ ],
193
+ changes: [
194
+ {
195
+ table: "tasks",
196
+ key: "task-1",
197
+ row: { ... } // or null to delete locally
198
+ }
199
+ ],
200
+ lastGlobalId: 12
201
+ }
202
+ ```
203
+
204
+ ##### Sync behavior
205
+
206
+ - Accepted local ids are removed from the pending queue.
207
+ - Conflicted local ids are removed from the pending queue.
208
+ - If a conflict includes `serverRow`, that row replaces the local optimistic row.
209
+ - Incoming `changes` are applied to the local replica.
210
+ - A `null` row in `changes` deletes the local row.
211
+ - The returned `lastGlobalId` becomes the client's new high-water mark.
212
+
213
+ #### `await client.exportState()`
214
+
215
+ Returns the persistence snapshot from the configured persistence layer.
216
+ With the built-in persistence implementations this includes:
217
+
218
+ - `clientSequence`
219
+ - `lastGlobalId`
220
+ - `pending`
221
+ - `rows`
222
+
223
+ ## Persistence Contract
224
+
225
+ The client currently expects a persistence object with these async methods:
226
+
227
+ - `initialize()`
228
+ - `nextClientSequence()`
229
+ - `getRow(table, key)`
230
+ - `listRows(table)`
231
+ - `saveRow(table, key, row)`
232
+ - `deleteRow(table, key)`
233
+ - `appendPendingMutation(mutation)`
234
+ - `getPendingMutations()`
235
+ - `acknowledgeMutations(localIds)`
236
+ - `getLastGlobalId()`
237
+ - `setLastGlobalId(lastGlobalId)`
238
+ - `exportState()`
239
+
240
+ The behavior expected from that contract is:
241
+
242
+ - rows are stored by table name and row key
243
+ - pending mutations are returned sorted by `clientSequence`
244
+ - `acknowledgeMutations()` removes queued mutation ids
245
+ - `setLastGlobalId()` replaces the current high-water mark
246
+ - `exportState()` returns a serializable snapshot of the persistence layer
247
+
248
+ ## `IndexedDbPersistence`
249
+
250
+ Browser persistence implementation backing the `"indexeddb"` built-in.
251
+
252
+ ```js
253
+ const persistence = new IndexedDbPersistence({
254
+ databaseName: "app-cache"
255
+ });
256
+ ```
257
+
258
+ ### Constructor options
259
+
260
+ - `databaseName`
261
+ Optional IndexedDB database name.
262
+ Defaults to `offlinedb`.
263
+ - `indexedDb`
264
+ Optional IndexedDB factory override.
265
+
266
+ ### Behavior
267
+
268
+ - wraps an in-memory working set and persists snapshots into IndexedDB
269
+ - creates a single object store named `state`
270
+ - stores the full persistence snapshot under the key `snapshot`
271
+ - loads the snapshot during `initialize()`
272
+ - persists after every mutating operation
273
+
274
+ `IndexedDbPersistence` is the default because it matches the browser-first, local-replica model of the library more closely than an in-memory-only store.
275
+
276
+ ## `LocalStoragePersistence`
277
+
278
+ Simple browser persistence implementation backing the `"localstorage"` built-in.
279
+
280
+ ```js
281
+ const persistence = new LocalStoragePersistence({
282
+ storageKey: "app-cache:snapshot"
283
+ });
284
+ ```
285
+
286
+ ### Constructor options
287
+
288
+ - `storageKey`
289
+ Optional localStorage key used for the serialized snapshot.
290
+ Defaults to `offlinedb:snapshot`.
291
+ - `localStorage`
292
+ Optional localStorage-compatible override.
293
+
294
+ ### Behavior
295
+
296
+ - wraps an in-memory working set
297
+ - serializes the full snapshot into one localStorage entry
298
+ - loads the snapshot during `initialize()`
299
+ - persists after every mutating operation
300
+
301
+ This is simpler than IndexedDB but less suitable for larger local replicas.
302
+
303
+ ## `InMemoryPersistence`
304
+
305
+ Reference in-memory persistence implementation used by the current tests and examples.
306
+
307
+ ```js
308
+ const persistence = new InMemoryPersistence();
309
+ ```
310
+
311
+ ### Behavior
312
+
313
+ - keeps one global sync `lastGlobalId` in process memory
314
+ - keeps one `lastGlobalId` value in process memory
315
+ - keeps all rows and pending mutations in process memory
316
+ - does not survive page reloads or process restarts
317
+ - remains useful for tests, examples, and non-browser flows
318
+
319
+ ## `createNeonHttpTransport(options)`
320
+
321
+ Creates a simple HTTP transport for a developer-supplied HTTP sync endpoint.
322
+
323
+ This helper still exists in the scaffold, but the intended Neon-native direction is direct Neon Auth and Data API calls via the `odb*` clients below rather than an app-owned sync service.
324
+
325
+ ```js
326
+ const transport = createNeonHttpTransport({
327
+ // Supplied by the app.
328
+ baseUrl: "https://example.neon.tech/sql",
329
+ getAuthToken: async () => sessionToken
330
+ });
331
+ ```
332
+
333
+ ### `options`
334
+
335
+ - `baseUrl`
336
+ Base URL used to build the sync endpoint URL.
337
+ - `fetchImpl`
338
+ Optional fetch implementation.
339
+ Defaults to global `fetch`.
340
+ - `getAuthToken`
341
+ Optional function or value used to populate the `Authorization` header.
342
+ When it returns a token, the transport sends `Authorization: Bearer <token>`.
343
+ - `syncPath`
344
+ Optional relative path appended to `baseUrl`.
345
+ Defaults to `rpc/offlinedb_sync`.
346
+
347
+ ### Behavior
348
+
349
+ - Sends `POST` with `content-type: application/json`
350
+ - Serializes the sync request body as JSON
351
+ - Throws when the HTTP response is not `2xx`
352
+ - Returns `await response.json()`
353
+
354
+ ## `createOdbAuthClient(options)`
355
+
356
+ Creates direct Neon Auth wrappers using `odb*` method names.
357
+
358
+ ```js
359
+ const auth = createOdbAuthClient({
360
+ baseUrl: appConfig.neonAuthUrl
361
+ });
362
+ ```
363
+
364
+ ### `options`
365
+
366
+ - `baseUrl`
367
+ Developer-supplied Neon Auth base URL.
368
+ - `fetchImpl`
369
+ Optional fetch implementation.
370
+ - `endpoints`
371
+ Optional endpoint overrides for auth routes.
372
+
373
+ ### Methods
374
+
375
+ - `odbLoginWithPassword({ email, password, rememberMe?, callbackURL? })`
376
+ - `odbSignUpWithPassword({ email, password, name?, callbackURL?, image? })`
377
+ - `odbLogout()`
378
+
379
+ ## `createOdbDataClient(options)`
380
+
381
+ Creates direct Neon Data API wrappers using `odb*` method names.
382
+
383
+ ```js
384
+ const data = createOdbDataClient({
385
+ baseUrl: appConfig.neonDataUrl,
386
+ getAuthToken: async () => sessionToken
387
+ });
388
+ ```
389
+
390
+ ### `options`
391
+
392
+ - `baseUrl`
393
+ Developer-supplied Neon Data API or SQL base URL.
394
+ - `fetchImpl`
395
+ Optional fetch implementation.
396
+ - `getAuthToken`
397
+ Optional async token provider.
398
+ - `headers`
399
+ Optional default headers.
400
+
401
+ ### Methods
402
+
403
+ - `odbRequest({ path, method?, body?, headers? })`
404
+ - `odbRpc(functionName, args?)`
405
+
406
+ ## `createOdbSyncTransport(options)`
407
+
408
+ Creates a direct-Neon sync transport that calls database RPC functions through `createOdbDataClient()`.
409
+
410
+ ```js
411
+ const data = createOdbDataClient({
412
+ baseUrl: appConfig.neonDataUrl,
413
+ getAuthToken: async () => sessionToken
414
+ });
415
+
416
+ const transport = createOdbSyncTransport({
417
+ dataClient: data
418
+ });
419
+ ```
420
+
421
+ ### `options`
422
+
423
+ - `dataClient`
424
+ Data client created by `createOdbDataClient()`.
425
+ - `mutateFunctionName`
426
+ Optional function-name override or name resolver for per-table mutate RPCs.
427
+ Defaults to `mutate_<table>`.
428
+ - `pullFunctionName`
429
+ Optional pull RPC name.
430
+ Defaults to `pull_changes`.
431
+
432
+ ### Behavior
433
+
434
+ - calls `mutate_<table>` once per pending local change
435
+ - then calls `pull_changes(lastGlobalId, tables?)`
436
+ - returns the normalized sync result expected by `OfflineDbClient`
437
+
438
+ This is the intended browser runtime path for the Neon-native version of the library.
439
+
440
+ ## `defineTable(input)`
441
+
442
+ Creates a normalized table definition object.
443
+
444
+ ```js
445
+ const tasks = defineTable({
446
+ name: "tasks",
447
+ primaryKey: "id",
448
+ ownerColumn: "owner_id",
449
+ revisionColumn: "revision",
450
+ updatedAtColumn: "updated_at",
451
+ localIdColumn: "local_id",
452
+ globalIdColumn: "global_id"
453
+ });
454
+ ```
455
+
456
+ ### `input`
457
+
458
+ - `name`
459
+ Required SQL-style identifier.
460
+ - `primaryKey`
461
+ Optional, defaults to `id`.
462
+ - `ownerColumn`
463
+ Optional, defaults to `owner_id`.
464
+ - `revisionColumn`
465
+ Optional, defaults to `revision`.
466
+ - `updatedAtColumn`
467
+ Optional, defaults to `updated_at`.
468
+ - `localIdColumn`
469
+ Optional, defaults to `local_id`.
470
+ - `globalIdColumn`
471
+ Optional, defaults to `global_id`.
472
+
473
+ ### Return value
474
+
475
+ Returns a frozen object:
476
+
477
+ ```js
478
+ {
479
+ kind: "table",
480
+ name,
481
+ primaryKey,
482
+ ownerColumn,
483
+ revisionColumn,
484
+ updatedAtColumn,
485
+ localIdColumn,
486
+ globalIdColumn
487
+ }
488
+ ```
489
+
490
+ All identifiers must match the current validation rule:
491
+
492
+ ```txt
493
+ /^[A-Za-z_][A-Za-z0-9_]*$/
494
+ ```
495
+
496
+ ## `createOfflinedbInstallSql(options?)`
497
+
498
+ Generates SQL for the shared metadata schema.
499
+
500
+ ```js
501
+ const sql = createOfflinedbInstallSql();
502
+ ```
503
+
504
+ ```js
505
+ const sql = createOfflinedbInstallSql({
506
+ schema: "offlinedb"
507
+ });
508
+ ```
509
+
510
+ ### `options`
511
+
512
+ - `schema`
513
+ Metadata schema name.
514
+ Defaults to `offlinedb`.
515
+
516
+ ### Generated objects
517
+
518
+ The current output includes:
519
+
520
+ - metadata schema creation
521
+ - `global_mutation` table
522
+ - `applied_mutation` table
523
+ - `current_user_id()` function
524
+ - `record_change()` trigger function
525
+
526
+ ### Current assumptions
527
+
528
+ - `current_user_id()` resolves the user from JWT claim settings in Postgres.
529
+ - `record_change()` expects synced tables to expose:
530
+ - `id`
531
+ - `local_id`
532
+ - `owner_id`
533
+ - `revision`
534
+
535
+ That expectation is important because the helper is intentionally narrow in this first scaffold.
536
+
537
+ ## `createSyncedTableSql(options)`
538
+
539
+ Generates SQL to prepare one app table for `offlinedb`.
540
+
541
+ ```js
542
+ const sql = createSyncedTableSql({
543
+ table: defineTable({ name: "tasks" })
544
+ });
545
+ ```
546
+
547
+ ```js
548
+ const sql = createSyncedTableSql({
549
+ schema: "public",
550
+ metadataSchema: "offlinedb",
551
+ table: defineTable({
552
+ name: "tasks",
553
+ primaryKey: "id"
554
+ })
555
+ });
556
+ ```
557
+
558
+ ### `options`
559
+
560
+ - `table`
561
+ Required table definition object or table name string.
562
+ - `schema`
563
+ App-table schema name.
564
+ Defaults to `public`.
565
+ - `metadataSchema`
566
+ Metadata schema containing helper functions.
567
+ Defaults to `offlinedb`.
568
+
569
+ ### Generated SQL responsibilities
570
+
571
+ The current generator emits SQL for:
572
+
573
+ - required sync metadata columns
574
+ - owner/global-id index
575
+ - revision and updated-at trigger function
576
+ - global-mutation capture trigger
577
+ - owner-write insert/update RLS policies
578
+ - a table-specific mutation RPC function named like `offlinedb.mutate_tasks`
579
+
580
+ ### Current caveats
581
+
582
+ - The table must use a single primary key column.
583
+ - The generated mutation RPC expects a full next-row payload in `p_values`.
584
+ - Accepted writes are assigned a server-side `globalId`.
585
+ - Public-read policies are not yet generated automatically.
586
+ - The shared `record_change()` helper currently assumes column names compatible with the default owner/id/local_id/revision pattern.
587
+
588
+ ## Current limitations of the public API
589
+
590
+ These are real current limits of the implementation:
591
+
592
+ - There is no built-in subscription API yet.
593
+ - There is no exported TypeScript typing surface.
594
+ - There is no finalized live Neon wire contract in this repo yet.
595
+ - Delete operations are only represented through remote `changes` entries with `row: null`; there is no dedicated local delete helper.
@@ -0,0 +1,29 @@
1
+ # Architecture
2
+
3
+ `offlinedb` is a browser-first offline replication library for Neon Auth and Postgres RLS.
4
+
5
+ ## Core model
6
+
7
+ - App tables remain ordinary Postgres tables.
8
+ - Every synced table has a non-null owner column.
9
+ - Security is enforced by Postgres RLS, not by a separate library ACL layer.
10
+ - Writes flow through library-owned SQL functions so revision checks and local-id/global-id assignment stay consistent.
11
+ - The default client model is a full local replica for all synced tables the user can read.
12
+ - Sync can be limited to selected tables as an optimization, but whole-replica sync is the default.
13
+
14
+ ## Runtime flow
15
+
16
+ 1. App code defines synced tables.
17
+ 2. The client stores a local replica plus a pending local-change log.
18
+ 3. Local writes apply optimistically to the replica and receive a client-assigned `localId`.
19
+ 4. `sync()` sends pending changes plus the client's `lastGlobalId`.
20
+ 5. Server-side SQL functions validate each change, assign accepted changes a global `globalId`, and record them in the global mutation log.
21
+ 6. The remote side returns accepted local/global id mappings, conflicts, changed rows, and a new `lastGlobalId`.
22
+ 7. The client updates the local replica, clears acknowledged local changes, and advances its global high-water mark.
23
+
24
+ ## Security boundary
25
+
26
+ - Neon Auth session state identifies the current user.
27
+ - Database functions resolve the current user from JWT claims.
28
+ - RLS policies decide row visibility and writability.
29
+ - The library assumes owner-write behavior and optional public readability.
@@ -0,0 +1,20 @@
1
+ # Decisions
2
+
3
+ ## Chosen defaults
4
+
5
+ - Neon Auth is the initial auth target.
6
+ - Direct client-to-Neon access is the primary data path.
7
+ - The library is browser-first.
8
+ - The first data model is relational row/table sync, not a document abstraction.
9
+ - Every synced table must include a non-null owner column.
10
+ - Rows may be private or publicly readable, but only owners write in v1.
11
+ - Conflicts are explicit per-row revision conflicts.
12
+ - The library owns the mutation RPC path for synced writes.
13
+ - Sync is framed as local-id to global-id convergence against a global mutation log.
14
+
15
+ ## Intentional non-goals for this pass
16
+
17
+ - Full Neon integration tests against a live database
18
+ - General-purpose SQL query caching
19
+ - Shared workspace role models beyond owner-centric tables
20
+ - Automatic conflict merging
package/docs/SQL.md ADDED
@@ -0,0 +1,38 @@
1
+ # SQL Helpers
2
+
3
+ `src/sql.js` currently generates two SQL bundles.
4
+
5
+ ## `createOfflinedbInstallSql()`
6
+
7
+ Creates metadata objects under the `offlinedb` schema:
8
+
9
+ - `global_mutation`
10
+ - `applied_mutation`
11
+ - `current_user_id()`
12
+ - `record_change()`
13
+
14
+ ## `createSyncedTableSql()`
15
+
16
+ Generates per-table SQL for:
17
+
18
+ - required sync metadata columns
19
+ - revision/update trigger
20
+ - global-mutation trigger
21
+ - owner-scoped RLS policies for `SELECT`, `INSERT`, `UPDATE`, and `DELETE`
22
+ - a table-specific mutation RPC function
23
+
24
+ ## Caveats
25
+
26
+ This is the first scaffold, not the final migration system.
27
+ The generated SQL is intentionally narrow:
28
+
29
+ - it assumes owner-centric tables
30
+ - it assumes a single primary key column
31
+ - it generates a table-specific mutation function
32
+ - it expects the mutation payload to contain the full next row image
33
+ - it uses `local_id` and `global_id` columns to represent unsynced and globally accepted writes
34
+ - it does not yet generate public-read, admin-read, or admin-write policies automatically
35
+ - it does not configure grants, exposed schemas, CORS, or other Neon platform settings
36
+
37
+
38
+
@@ -0,0 +1,10 @@
1
+ {
2
+ "metadataSchema": "offlinedb",
3
+ "schema": "public",
4
+ "tables": [
5
+ {
6
+ "name": "tasks",
7
+ "primaryKey": "id"
8
+ }
9
+ ]
10
+ }