@decaf-ts/for-pouch 0.2.8 → 0.2.9

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
@@ -1,6 +1,8 @@
1
- ![Banner](./workdocs/assets/Banner.png)
1
+ # decaf-ts / for-pouch
2
+
3
+ ## Purpose at a Glance
4
+ A PouchDB-backed adapter and repository integration for the decaf-ts ecosystem. It provides a Repository implementation powered by PouchDB/CouchDB features (Mango queries, indexes, bulk ops, and relations), along with configuration types and constants to wire models to a PouchDB database (local or remote) using decorators.
2
5
 
3
- # Decaf's PouchDB (CouchDB) Module
4
6
 
5
7
  ![Licence](https://img.shields.io/github/license/decaf-ts/for-pouch.svg?style=plastic)
6
8
  ![GitHub language count](https://img.shields.io/github/languages/count/decaf-ts/for-pouch?style=plastic)
@@ -25,29 +27,400 @@
25
27
 
26
28
  Documentation available [here](https://decaf-ts.github.io/for-pouch/)
27
29
 
28
- ### Description
30
+ # decaf-ts / for-pouch — Detailed Description
31
+
32
+ This package integrates PouchDB with the decaf-ts data and decorator ecosystem. It provides:
33
+ - A concrete PouchAdapter that implements persistence against a PouchDB backend (local or remote CouchDB-compatible server).
34
+ - A typed PouchRepository alias for convenience when working with decaf-ts Repository and Mango queries.
35
+ - Configuration and flag types tailored for PouchDB usage.
36
+ - A module entry that wires flavour-specific decorations for createdBy/updatedBy when the module is loaded.
37
+
38
+ The intent of this library is to offer an ergonomic, type-safe repository pattern on top of PouchDB/CouchDB, including:
39
+ - CRUD operations (single and bulk) with proper error mapping.
40
+ - Query support via Mango queries, sorting with defined indexes, and pagination via core utilities.
41
+ - Support for multiple databases and aliases.
42
+ - Seamless model decoration with decaf-ts decorators, including created/updated metadata and relation handling.
43
+
44
+
45
+ API Inventory by File
46
+
47
+ 1) src/constants.ts
48
+ - PouchFlavour: string = "pouch" — Flavour identifier used by the decorator system and Repository.forModel resolution.
49
+ - DefaultLocalStoragePath: string = "local_dbs" — Default path for local PouchDB storage when running without a remote host.
50
+
51
+ 2) src/types.ts
52
+ - interface PouchFlags extends RepositoryFlags
53
+ - UUID: string — a per-operation/user identifier injected in Context and used by createdBy/updatedBy decoration.
54
+ - type PouchConfig
55
+ - user?: string — remote username.
56
+ - password?: string — remote password.
57
+ - host?: string — remote host.
58
+ - protocol?: "http" | "https" — remote protocol.
59
+ - port?: number — remote port (optional if in host).
60
+ - dbName: string — database name.
61
+ - storagePath?: string — base path for local databases.
62
+ - plugins: any[] — list of PouchDB plugins to register before client creation.
63
+
64
+ 3) src/PouchRepository.ts
65
+ - type PouchRepository<M extends Model> = Repository<M, MangoQuery, PouchAdapter>
66
+ - Convenience alias that binds the decaf-ts Repository with MangoQuery and the PouchAdapter backend.
67
+
68
+ 4) src/adapter.ts
69
+ - function createdByOnPouchCreateUpdate<M, R, V>(this: R, context: Context<PouchFlags>, data: V, key: keyof M, model: M): Promise<void>
70
+ - Decorator handler: copies context UUID into the model[key]. Throws UnsupportedError when unavailable.
71
+ - class PouchAdapter extends CouchDBAdapter<PouchConfig, PouchDB.Database, PouchFlags, Context<PouchFlags>>
72
+ - constructor(config: PouchConfig, alias?: string)
73
+ - Initializes the adapter with configuration and optional alias.
74
+ - getClient(): PouchDB.Database
75
+ - Lazy client getter; registers provided plugins; creates local or remote client.
76
+ - flags(operation, model, flags?): Context<PouchFlags>
77
+ - Prepares operation context and attaches Pouch-specific flags when required.
78
+ - index(models: Constructor<Model>[]): Promise<CreateIndexResponse[]>
79
+ - Generates remote/local indexes based on @index decorators in the given models.
80
+ - initialize(): Promise<CreateIndexResponse[]>
81
+ - Inherited via CouchDBAdapter; here used in tests to create indexes for sorting. (Called on the adapter instance.)
82
+ - create(tableName: string, id: Id, model: Model): Promise<Model>
83
+ - createAll(tableName: string, ids: Id[], models: Model[]): Promise<Model[]>
84
+ - read(tableName: string, id: Id): Promise<Model>
85
+ - readAll(tableName: string, ids: Id[]): Promise<Model[]>
86
+ - update(tableName: string, id: Id, model: Model): Promise<Model>
87
+ - updateAll(tableName: string, ids: Id[], models: Model[]): Promise<Model[]>
88
+ - delete(tableName: string, id: Id): Promise<Model>
89
+ - deleteAll(tableName: string, ids: Id[]): Promise<Model[]>
90
+ - Bulk variants aggregate item-level errors and throw a mapped BaseError when any failures occur.
91
+ - raw<T = any>(rawInput: any, process: boolean): Promise<T>
92
+ - Executes a raw Mango find request. When process=true, returns docs array; otherwise returns full find response.
93
+ - static parseError(err: unknown): BaseError
94
+ - Maps PouchDB/HTTP errors and messages into decaf-ts BaseError subtypes, including ConflictError/NotFoundError/ConnectionError.
95
+ - The instance method parseError delegates to the static implementation.
96
+ - static decoration(): void
97
+ - Registers createdByOnPouchCreateUpdate for the pouch flavour so createdBy/updatedBy fields are managed automatically.
98
+
99
+ 5) src/index.ts
100
+ - Side-effect call: PouchAdapter.decoration() — ensures flavour-specific decorator handler is registered upon import.
101
+ - Re-exports: constants, PouchRepository, types, adapter.
102
+ - VERSION: string — package version placeholder replaced at build time.
103
+
104
+
105
+ Behavioral Notes and Design Intent
106
+
107
+ - Multiple DB support: A PouchAdapter can be constructed with an alias; Repository.forModel(Model, alias) resolves the repository for that specific adapter/DB. This enables working with multiple databases concurrently.
108
+ - Decorator-driven modeling: Use @model, @pk, @index, @readonly, and other decaf-ts decorators to describe schemas and constraints. The adapter interprets indexes through @index and can generate them via initialize() or index().
109
+ - Querying: The core Repository composes Mango queries via select().where(Condition...).orderBy(...). PouchAdapter translates and executes these queries with PouchDB Find.
110
+ - Pagination: Use core Paginator returned by paginate(size) on a selection. Sorting requires proper indexes.
111
+ - Error translation: PouchAdapter.parseError normalizes errors from PouchDB/CouchDB and HTTP status codes into a stable error hierarchy for consistent handling.
112
+ - Raw access: raw() allows advanced Mango usage or debugging by running low-level queries and choosing between processed docs or the full response.
113
+
114
+
115
+ # How to Use decaf-ts / for-pouch
116
+
117
+ Below are practical, valid TypeScript examples based on the repository’s tests. They cover the exported APIs of this package without duplication.
118
+
119
+ ## 1) Install and Initialize a PouchAdapter
120
+
121
+ You can work with a local/in-memory database (useful for tests) or a remote CouchDB-compatible server.
122
+
123
+ ```ts
124
+ import { PouchAdapter, DefaultLocalStoragePath, VERSION } from "@decaf-ts/for-pouch";
125
+
126
+ // Example: Local (in-memory) PouchDB using the memory adapter plugin
127
+ async function makeMemoryAdapter() {
128
+ const memory = (await import("pouchdb-adapter-memory")).default as any;
129
+ // Alias allows multiple DBs; useful in multi-tenant scenarios
130
+ const adapter = new PouchAdapter({ dbName: "local_mem_db", plugins: [memory] }, "mem-local");
131
+ // Accessing the client verifies plugins and initializes the PouchDB instance
132
+ const client: any = (adapter as any).client;
133
+ return adapter;
134
+ }
135
+
136
+ // Example: Remote CouchDB-compatible server
137
+ async function makeRemoteAdapter() {
138
+ const adapter = new PouchAdapter(
139
+ {
140
+ protocol: "http",
141
+ host: "localhost:5984",
142
+ user: "admin",
143
+ password: "secret",
144
+ dbName: "my_database",
145
+ plugins: [],
146
+ },
147
+ "remote-1"
148
+ );
149
+ return adapter;
150
+ }
151
+
152
+ console.log("for-pouch version:", VERSION);
153
+ ```
154
+
155
+ ## 2) Model Definition with Decorators
156
+
157
+ Use decaf-ts decorators to define your schema, indexes, and target flavour. The @uses("pouch") decorator ties the model to this adapter flavour.
158
+
159
+ ```ts
160
+ import {
161
+ BaseModel,
162
+ Repository,
163
+ OrderDirection,
164
+ pk,
165
+ index,
166
+ uses,
167
+ } from "@decaf-ts/core";
168
+ import {
169
+ Model,
170
+ model,
171
+ required,
172
+ minlength,
173
+ min,
174
+ type,
175
+ ModelArg,
176
+ } from "@decaf-ts/decorator-validation";
177
+
178
+ @uses("pouch")
179
+ @model()
180
+ class User extends BaseModel {
181
+ @pk({ type: "Number" })
182
+ id!: number;
183
+
184
+ @required()
185
+ @min(18)
186
+ @index([OrderDirection.DSC, OrderDirection.ASC])
187
+ age!: number;
188
+
189
+ @required()
190
+ @minlength(5)
191
+ name!: string;
192
+
193
+ @required()
194
+ @type([String.name])
195
+ sex!: "M" | "F";
196
+
197
+ constructor(arg?: ModelArg<User>) {
198
+ super(arg);
199
+ }
200
+ }
201
+
202
+ Model.setBuilder(Model.fromModel);
203
+ ```
204
+
205
+ ## 3) Basic CRUD with Repository and PouchAdapter
206
+
207
+ ```ts
208
+ import { Repository } from "@decaf-ts/core";
209
+ import { PouchAdapter } from "@decaf-ts/for-pouch";
210
+
211
+ async function crudExample(adapter: PouchAdapter) {
212
+ const repo = new Repository(adapter, User);
213
+
214
+ // Create
215
+ const created = await repo.create(
216
+ new User({ name: "user_name_1", age: 20, sex: "M" })
217
+ );
218
+
219
+ // Read
220
+ const read = await repo.read(created.id);
221
+
222
+ // Update
223
+ const updated = await repo.update(new User({ ...created, name: "new_name" }));
224
+
225
+ // Delete
226
+ const deleted = await repo.delete(created.id);
227
+
228
+ return { created, read, updated, deleted };
229
+ }
230
+ ```
231
+
232
+ ## 4) Bulk Operations (createAll, readAll, updateAll, deleteAll)
233
+
234
+ ```ts
235
+ async function bulkExample(adapter: PouchAdapter) {
236
+ const repo = new Repository(adapter, User);
237
+
238
+ // Create many
239
+ const models = Array.from({ length: 5 }, (_, i) =>
240
+ new User({ name: `user_${i + 1}`.padEnd(6, "_"), age: 18 + i, sex: i % 2 ? "F" : "M" })
241
+ );
242
+ const created = await repo.createAll(models);
243
+
244
+ // Read many by id
245
+ const ids = created.map((u) => u.id);
246
+ const many = await repo.readAll(ids);
247
+
248
+ // Update many
249
+ const updated = await repo.updateAll(
250
+ many.map((u) => new User({ ...u, name: u.name + "_x" }))
251
+ );
252
+
253
+ // Delete many
254
+ const deleted = await repo.deleteAll(updated.map((u) => u.id));
255
+ return { created, many, updated, deleted };
256
+ }
257
+ ```
258
+
259
+ Notes:
260
+ - Bulk methods aggregate item-level errors; if any operation fails, an error mapped via parseError is thrown.
261
+
262
+ ## 5) Querying with select(), where(), and orderBy()
263
+
264
+ ```ts
265
+ import { Condition, OrderDirection } from "@decaf-ts/core";
266
+
267
+ async function queryExample(adapter: PouchAdapter) {
268
+ const repo = new Repository(adapter, User);
269
+
270
+ // Insert sample data
271
+ await repo.createAll(
272
+ [1, 2, 3, 4, 5].map((i) => new User({ name: `user_name_${i}`, age: 18 + i % 3, sex: i % 2 ? "F" : "M" }))
273
+ );
274
+
275
+ // Fetch full objects
276
+ const all = await repo.select().execute();
277
+
278
+ // Fetch only selected attributes
279
+ const projected = await repo.select(["age", "sex"]).execute();
280
+
281
+ // Conditional filtering
282
+ const cond = Condition.attribute<User>("age").eq(20);
283
+ const exactly20 = await repo.select().where(cond).execute();
284
+
285
+ // Sorting requires proper indexes (use adapter.initialize() to build from @index decorators)
286
+ await adapter.initialize();
287
+ const sorted = await repo.select().orderBy(["age", OrderDirection.DSC]).execute();
288
+
289
+ return { all, projected, exactly20, sorted };
290
+ }
291
+ ```
292
+
293
+ ## 6) Pagination
294
+
295
+ ```ts
296
+ import { Paginator } from "@decaf-ts/core";
29
297
 
30
- No one needs the hassle of setting up new repos every time.
298
+ async function paginationExample(adapter: PouchAdapter) {
299
+ const repo = new Repository(adapter, User);
31
300
 
32
- Now you can create new repositories from this template and enjoy having everything set up for you.
301
+ await adapter.initialize();
302
+ const paginator: Paginator<User, any> = await repo
303
+ .select()
304
+ .orderBy(["id", OrderDirection.DSC])
305
+ .paginate(10);
33
306
 
307
+ const page1 = await paginator.page();
308
+ const page2 = await paginator.next();
309
+ return { page1, page2 };
310
+ }
311
+ ```
34
312
 
313
+ ## 7) Multiple Databases via Alias
35
314
 
36
- ### How to Use
315
+ ```ts
316
+ import { Repository } from "@decaf-ts/core";
317
+ import { PouchAdapter } from "@decaf-ts/for-pouch";
37
318
 
38
- - [Initial Setup](./tutorials/For%20Developers.md#_initial-setup_)
39
- - [Installation](./tutorials/For%20Developers.md#installation)
40
- - [Scripts](./tutorials/For%20Developers.md#scripts)
41
- - [Linting](./tutorials/For%20Developers.md#testing)
42
- - [CI/CD](./tutorials/For%20Developers.md#continuous-integrationdeployment)
43
- - [Publishing](./tutorials/For%20Developers.md#publishing)
44
- - [Structure](./tutorials/For%20Developers.md#repository-structure)
45
- - [IDE Integrations](./tutorials/For%20Developers.md#ide-integrations)
46
- - [VSCode(ium)](./tutorials/For%20Developers.md#visual-studio-code-vscode)
47
- - [WebStorm](./tutorials/For%20Developers.md#webstorm)
48
- - [Considerations](./tutorials/For%20Developers.md#considerations)
319
+ async function multiDbExample() {
320
+ const memory = (await import("pouchdb-adapter-memory")).default as any;
49
321
 
322
+ // Two adapters with distinct aliases
323
+ const db1 = new PouchAdapter({ dbName: "db1", plugins: [memory] }, "db1");
324
+ const db2 = new PouchAdapter({ dbName: "db2", plugins: [memory] }, "db2");
325
+
326
+ // Repository.forModel can resolve by alias (after @uses("pouch") on the model)
327
+ const repo1 = Repository.forModel(User, "db1");
328
+ const repo2 = Repository.forModel(User, "db2");
329
+
330
+ const u1 = await repo1.create(new User({ name: "A_user", age: 21, sex: "M" }));
331
+ const u2 = await repo2.create(new User({ name: "B_user", age: 22, sex: "F" }));
332
+
333
+ const again1 = await repo1.read(u1.id);
334
+ const again2 = await repo2.read(u2.id);
335
+ return { again1, again2 };
336
+ }
337
+ ```
338
+
339
+ ## 8) Using raw() for Advanced Mango Queries
340
+
341
+ ```ts
342
+ import { CouchDBKeys } from "@decaf-ts/for-couchdb";
343
+
344
+ async function rawExample(adapter: PouchAdapter) {
345
+ const client: any = (adapter as any).client;
346
+ await client.put({ [CouchDBKeys.ID]: "r1", type: "row", x: 1 });
347
+ await client.put({ [CouchDBKeys.ID]: "r2", type: "row", x: 2 });
348
+
349
+ // process=true -> returns docs array only
350
+ const docsOnly = await adapter.raw<any[]>({ selector: { type: { $eq: "row" } } }, true);
351
+
352
+ // process=false -> returns the full FindResponse
353
+ const full = await adapter.raw<any>({ selector: { type: { $eq: "row" } } }, false);
354
+
355
+ return { docsOnly, full };
356
+ }
357
+ ```
50
358
 
359
+ ## 9) Error Handling with parseError
360
+
361
+ ```ts
362
+ import { BaseError } from "@decaf-ts/db-decorators";
363
+ import { PouchAdapter } from "@decaf-ts/for-pouch";
364
+
365
+ async function parseErrorExample(adapter: PouchAdapter) {
366
+ try {
367
+ await adapter.read("tbl", "no-such-id");
368
+ } catch (e) {
369
+ // Convert low-level errors to decaf-ts BaseError shape
370
+ const parsed = PouchAdapter.parseError(e);
371
+ if (parsed instanceof BaseError) {
372
+ // handle known error types (ConflictError, NotFoundError, etc.)
373
+ console.warn("Handled decaf error:", parsed.message);
374
+ } else {
375
+ throw e;
376
+ }
377
+ }
378
+ }
379
+ ```
380
+
381
+ ## 10) createdBy/updatedBy Handling via Context Flags
382
+
383
+ The module registers a handler that copies a context UUID into the createdBy/updatedBy fields for the pouch flavour. In advanced cases you can call the handler directly, as shown in tests.
384
+
385
+ ```ts
386
+ import { createdByOnPouchCreateUpdate, PouchFlags } from "@decaf-ts/for-pouch";
387
+ import { Context } from "@decaf-ts/db-decorators";
388
+
389
+ class ExampleModel { createdBy?: string }
390
+
391
+ async function createdByExample() {
392
+ const ctx = new Context<PouchFlags>().accumulate({ UUID: "user-123" });
393
+ const model = new ExampleModel();
394
+ await createdByOnPouchCreateUpdate.call(
395
+ {} as any,
396
+ ctx,
397
+ {} as any,
398
+ "createdBy" as any,
399
+ model as any
400
+ );
401
+ // model.createdBy === "user-123"
402
+ return model;
403
+ }
404
+ ```
405
+
406
+ ## 11) Types and Constants
407
+
408
+ ```ts
409
+ import type { PouchConfig, PouchFlags } from "@decaf-ts/for-pouch";
410
+ import { PouchFlavour, DefaultLocalStoragePath } from "@decaf-ts/for-pouch";
411
+
412
+ const flavour: string = PouchFlavour; // "pouch"
413
+ const defaultPath: string = DefaultLocalStoragePath; // "local_dbs"
414
+
415
+ const cfg: PouchConfig = {
416
+ dbName: "sample",
417
+ plugins: [],
418
+ };
419
+
420
+ const flags: PouchFlags = {
421
+ UUID: "user-xyz",
422
+ };
423
+ ```
51
424
 
52
425
 
53
426
  ### Related