$npx -y skills add DanWahlin/github-azure-agentic-journeys --skill data-access-abstractionData access abstraction patterns for apps that need to swap between local databases (SQLite) and cloud databases (Cosmos DB, PostgreSQL) without changing application code. Covers Node.js/TypeScript, Python/FastAPI, and .NET. Use when building apps that run locally with SQLite and
| 1 | # Data Access Abstraction |
| 2 | |
| 3 | Build APIs with swappable data layers using the repository pattern. Develop locally with SQLite, deploy to Azure with Cosmos DB or PostgreSQL — same route code, different backend. Works in any language. |
| 4 | |
| 5 | ## When to Use This Skill |
| 6 | |
| 7 | - Building an API that needs to run locally (SQLite) and in Azure (Cosmos DB or PostgreSQL) |
| 8 | - Adding a new data provider to an existing app without changing routes or business logic |
| 9 | - Migrating from one database to another incrementally |
| 10 | - Any project following the AIMarket journey pattern (local dev → cloud deploy) |
| 11 | |
| 12 | ## Pattern Overview |
| 13 | |
| 14 | The pattern is the same regardless of language: |
| 15 | |
| 16 | ``` |
| 17 | Routes/Controllers → Repository Interfaces → Factory → Implementations |
| 18 | │ |
| 19 | ├── SQLite (local dev) |
| 20 | ├── Cosmos DB (Azure deploy) |
| 21 | ├── PostgreSQL (Azure deploy) |
| 22 | └── In-memory (testing) |
| 23 | ``` |
| 24 | |
| 25 | **Three rules:** |
| 26 | 1. Define repository interfaces (or abstract classes / protocols) per entity |
| 27 | 2. Routes depend only on the interfaces — never import a database client directly |
| 28 | 3. A factory reads a config value (`DATA_PROVIDER`) and returns the right implementation |
| 29 | |
| 30 | Adding a new database means writing a new implementation file. Zero changes to routes. |
| 31 | |
| 32 | ## Environment Variable |
| 33 | |
| 34 | All languages use the same convention: |
| 35 | |
| 36 | ``` |
| 37 | DATA_PROVIDER=sqlite # Local development (default) |
| 38 | DATA_PROVIDER=cosmos # Azure Cosmos DB |
| 39 | DATA_PROVIDER=postgres # Azure PostgreSQL |
| 40 | ``` |
| 41 | |
| 42 | --- |
| 43 | |
| 44 | ## Node.js / TypeScript |
| 45 | |
| 46 | ### Repository Interfaces |
| 47 | |
| 48 | ```typescript |
| 49 | // data/interfaces.ts |
| 50 | |
| 51 | export interface IProductRepository { |
| 52 | getAll(params: { |
| 53 | page: number; pageSize: number; |
| 54 | category?: string; minPrice?: number; maxPrice?: number; |
| 55 | }): Promise<{ data: Product[]; totalCount: number }>; |
| 56 | |
| 57 | getById(id: string): Promise<Product | null>; |
| 58 | create(input: CreateProductInput): Promise<Product>; |
| 59 | update(id: string, fields: Partial<Product>): Promise<Product | null>; |
| 60 | } |
| 61 | |
| 62 | export interface IOrderRepository { |
| 63 | create(input: CreateOrderInput): Promise<Order>; |
| 64 | getById(id: string): Promise<Order | null>; |
| 65 | getByUserId(userId: string, page: number, pageSize: number): Promise<{ data: Order[]; totalCount: number }>; |
| 66 | } |
| 67 | |
| 68 | export interface IUserRepository { |
| 69 | create(input: CreateUserInput): Promise<User>; |
| 70 | getById(id: string): Promise<User | null>; |
| 71 | getByEmail(email: string): Promise<User | null>; |
| 72 | } |
| 73 | |
| 74 | export interface DataStore { |
| 75 | products: IProductRepository; |
| 76 | orders: IOrderRepository; |
| 77 | users: IUserRepository; |
| 78 | close?(): void; |
| 79 | } |
| 80 | ``` |
| 81 | |
| 82 | ### Factory |
| 83 | |
| 84 | ```typescript |
| 85 | // data/store.ts |
| 86 | |
| 87 | export async function createStore(): Promise<DataStore> { |
| 88 | const provider = process.env.DATA_PROVIDER || 'sqlite'; |
| 89 | switch (provider) { |
| 90 | case 'sqlite': |
| 91 | return (await import('./sqlite.js')).createSqliteStore(); |
| 92 | case 'cosmos': |
| 93 | return (await import('./cosmos.js')).createCosmosStore(); |
| 94 | case 'postgres': |
| 95 | return (await import('./postgres.js')).createPostgresStore(); |
| 96 | default: |
| 97 | throw new Error(`Unknown DATA_PROVIDER: ${provider}`); |
| 98 | } |
| 99 | } |
| 100 | ``` |
| 101 | |
| 102 | ### SQLite Implementation |
| 103 | |
| 104 | Use `better-sqlite3` (synchronous API, fast). Wrap sync calls in `Promise.resolve()` or make routes `await` — sync values resolve immediately. |
| 105 | |
| 106 | ```typescript |
| 107 | // data/sqlite.ts |
| 108 | |
| 109 | import Database from 'better-sqlite3'; |
| 110 | |
| 111 | export function createSqliteStore(dbPath = 'app.db'): DataStore { |
| 112 | const db = new Database(dbPath); |
| 113 | db.pragma('journal_mode = WAL'); |
| 114 | db.pragma('foreign_keys = ON'); |
| 115 | // Create tables, seed if empty... |
| 116 | |
| 117 | return { |
| 118 | products: { |
| 119 | async getAll({ page, pageSize, category, minPrice, maxPrice }) { |
| 120 | let where = 'WHERE status = ?'; |
| 121 | const params: any[] = ['active']; |
| 122 | if (category) { where += ' AND category = ?'; params.push(category); } |
| 123 | if (minPrice != null) { where += ' AND price >= ?'; params.push(minPrice); } |
| 124 | if (maxPrice != null) { where += ' AND price <= ?'; params.push(maxPrice); } |
| 125 | |
| 126 | const { c: totalCount } = db.prepare(`SELECT COUNT(*) as c FROM products ${where}`).get(...params); |
| 127 | const rows = db.prepare(`SELECT * FROM products ${where} LIMIT ? OFFSET ?`) |
| 128 | .all(...params, pageSize, (page - 1) * pageSize); |
| 129 | return { data: rows.map(parseRow), totalCount }; |
| 130 | }, |
| 131 | // ... |
| 132 | }, |
| 133 | close: () => db.close(), |
| 134 | }; |
| 135 | } |
| 136 | ``` |
| 137 | |
| 138 | **SQLite-specific:** Store arrays as JSON strings (`JSON.stringify(tags)`), parse on read. Use a junction table for order items. |
| 139 | |
| 140 | ### Cosmo |