@airdraft/db-adapter 0.1.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/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # @airdraft/db-adapter
2
+
3
+ Base package for Airdraft database adapters. Provides the abstract `BaseDatabaseAdapter` class, shared TypeScript types, and a portable contract test suite that all driver packages run against.
4
+
5
+ You never install this package directly in a user project. It is a peer dependency of each driver package (`@airdraft/db-adapter-sqlite`, `@airdraft/db-adapter-postgres`, `@airdraft/db-adapter-mongodb`).
6
+
7
+ ---
8
+
9
+ ## Architecture
10
+
11
+ Airdraft stores content as typed documents (entries). Each entry lives at a path `<collection>/<slug>`, carries an opaque `sha` (SHA-256 of the serialized content), and is tied to a `projectId` for multi-tenancy.
12
+
13
+ `BaseDatabaseAdapter` implements the full `StorageAdapter` interface expected by the CMS engine, so the engine, plugins, and client SDKs see no difference between a file adapter and a database adapter.
14
+
15
+ The engine detects a database adapter via `instanceof BaseDatabaseAdapter` and routes bulk list operations through the single-query `queryEntries()` method rather than the N+1 `list()` + `read()` loop used for file adapters.
16
+
17
+ ---
18
+
19
+ ## Abstract methods (implement in your driver)
20
+
21
+ | Method | Description |
22
+ |---|---|
23
+ | `migrate(): Promise<void>` | Creates `airdraft_entries` and (when `history: true`) `airdraft_entry_history`. Idempotent — safe to call on every deploy. |
24
+ | `read(path): Promise<FileResult \| null>` | Reads a single entry by path. |
25
+ | `write(path, content, options): Promise<void>` | Creates or updates an entry. Must enforce SHA-based optimistic concurrency and throw `ConflictError` on mismatch. |
26
+ | `delete(path, options): Promise<void>` | Deletes an entry. |
27
+ | `queryEntries(collection, options): Promise<QueryEntriesResult>` | Bulk query: filters, sorts, and paginates in a single DB call. Called by the engine fast-path. |
28
+ | `atomicUpdate(path, ops, sha): Promise<void>` | Applies `AtomicOp` field operations (increment / set / push / pull) atomically. |
29
+ | `rollback(path, sha): Promise<void>` | Restore a previous revision from the history table. Only when `history: true`. |
30
+ | `close(): Promise<void>` | Release connection pool / file handles. |
31
+
32
+ ---
33
+
34
+ ## Constructor options (`DbAdapterOptions`)
35
+
36
+ | Option | Type | Default | Description |
37
+ |---|---|---|---|
38
+ | `projectId` | `string` | `'default'` | Isolates all reads/writes within a shared DB instance. Set to the Airdraft project slug for multi-tenant deployments. |
39
+ | `history` | `boolean` | `false` | When `true`, mirrors every write to `airdraft_entry_history`. Enables `rollback()`. |
40
+ | `cacheTtlMs` | `number` | `0` | Per-entry read cache TTL in milliseconds. `0` disables the cache. |
41
+ | `cacheMaxSize` | `number` | `500` | Maximum entries in the LRU read cache. |
42
+
43
+ ---
44
+
45
+ ## Shared types
46
+
47
+ ```ts
48
+ import type { DbAdapterOptions, QueryEntriesResult, AtomicOp } from '@airdraft/db-adapter'
49
+ ```
50
+
51
+ ### `QueryEntriesResult`
52
+
53
+ ```ts
54
+ interface QueryEntriesResult {
55
+ entries: Array<{ path: string; content: string; sha: string }>
56
+ total: number // total matching rows before pagination — used for meta.total
57
+ }
58
+ ```
59
+
60
+ ### `AtomicOp`
61
+
62
+ ```ts
63
+ type AtomicOp =
64
+ | { op: 'increment'; path: string; by: number }
65
+ | { op: 'set'; path: string; value: unknown }
66
+ | { op: 'push'; path: string; value: unknown }
67
+ | { op: 'pull'; path: string; value: unknown }
68
+ ```
69
+
70
+ `path` uses dot-notation — e.g. `'stats.viewCount'` for a nested field.
71
+
72
+ ---
73
+
74
+ ## Contract test suite
75
+
76
+ The `@airdraft/db-adapter/testing` export provides a shared Vitest test suite that all driver packages run against to verify full contract compliance.
77
+
78
+ ```ts
79
+ // src/__tests__/contract.test.ts (inside a driver package)
80
+ import { describe } from 'vitest'
81
+ import { runStorageAdapterContract } from '@airdraft/db-adapter/testing'
82
+ import { SQLiteAdapter } from '../SQLiteAdapter.js'
83
+
84
+ describe('SQLiteAdapter contract', () => {
85
+ runStorageAdapterContract(() => new SQLiteAdapter({ filename: ':memory:', history: true }))
86
+ })
87
+ ```
88
+
89
+ The suite covers: `migrate`, `write`/`read`, `ConflictError` on SHA mismatch, `delete`, `list`, `queryEntries` (with filters, sort, pagination), `atomicUpdate`, history writes, and `rollback`.
90
+
91
+ ---
92
+
93
+ ## Writing a custom adapter
94
+
95
+ Extend `BaseDatabaseAdapter` and implement all abstract methods:
96
+
97
+ ```ts
98
+ import { BaseDatabaseAdapter } from '@airdraft/db-adapter'
99
+ import type { FileResult, WriteOptions, DeleteOptions, ListOptions } from '@airdraft/core'
100
+ import type { QueryEntriesResult, AtomicOp } from '@airdraft/db-adapter'
101
+
102
+ export class MyAdapter extends BaseDatabaseAdapter {
103
+ async migrate() { /* CREATE TABLE IF NOT EXISTS … */ }
104
+ async read(path: string): Promise<FileResult | null> { /* … */ }
105
+ async write(path: string, content: string, options: WriteOptions): Promise<void> { /* … */ }
106
+ async delete(path: string, options: DeleteOptions): Promise<void> { /* … */ }
107
+ async queryEntries(collection: string, options: ListOptions): Promise<QueryEntriesResult> { /* … */ }
108
+ async atomicUpdate(path: string, ops: AtomicOp[], sha: string): Promise<void> { /* … */ }
109
+ async rollback(path: string, sha: string): Promise<void> { /* … */ }
110
+ async close(): Promise<void> { /* … */ }
111
+ }
112
+ ```
113
+
114
+ Run the shared contract suite in your tests to confirm your implementation is spec-compliant.
115
+
116
+ ---
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1,73 @@
1
+ import type { StorageAdapter, FileResult, WriteOptions, DeleteOptions, FileListItem, ListOptions } from '@airdraft/core';
2
+ import { LruCache } from '@airdraft/core';
3
+ import type { DbAdapterOptions, QueryEntriesResult, AtomicOp } from './types.js';
4
+ /**
5
+ * Abstract base class for all Airdraft database adapters.
6
+ *
7
+ * Implements the full `StorageAdapter` interface so the engine, plugins,
8
+ * and client code see no difference between a file adapter and a DB adapter.
9
+ *
10
+ * Concrete driver classes (PostgresAdapter, SQLiteAdapter, MongooseAdapter)
11
+ * extend this class and implement the abstract methods.
12
+ */
13
+ export declare abstract class BaseDatabaseAdapter implements StorageAdapter {
14
+ protected readonly projectId: string;
15
+ protected readonly history: boolean;
16
+ protected readonly cache: LruCache<FileResult | null> | null;
17
+ constructor(options?: DbAdapterOptions);
18
+ abstract read(path: string): Promise<FileResult | null>;
19
+ abstract write(path: string, content: string, options: WriteOptions): Promise<void>;
20
+ abstract delete(path: string, options: DeleteOptions): Promise<void>;
21
+ /**
22
+ * Returns all entries for the collection identified by the first path
23
+ * segment of `glob`. Slug-level glob patterns are ignored.
24
+ *
25
+ * This method is never on the hot path when a DB adapter is in use —
26
+ * the engine calls `queryEntries()` directly for `listEntries()`.
27
+ * `list()` is only called by plugin infrastructure (audit log, search).
28
+ */
29
+ list(glob: string): Promise<FileListItem[]>;
30
+ /**
31
+ * Execute a native DB query for a collection.
32
+ * Called by the engine instead of the list+read loop when a DB adapter
33
+ * is detected via `instanceof BaseDatabaseAdapter`.
34
+ */
35
+ abstract queryEntries(collection: string, options: ListOptions): Promise<QueryEntriesResult>;
36
+ /**
37
+ * Create the `airdraft_entries` table / collection (and indexes, and the
38
+ * `airdraft_entry_history` table when `history: true`).
39
+ *
40
+ * Idempotent — safe to call on every deploy (e.g. from `instrumentation.ts`).
41
+ */
42
+ abstract migrate(): Promise<void>;
43
+ /**
44
+ * Apply one or more field-level operations atomically in a single DB op.
45
+ * The `sha` parameter is the current sha of the entry — throws `ConflictError`
46
+ * if it no longer matches (optimistic concurrency).
47
+ *
48
+ * Designed for ecommerce workloads: inventory decrement, order status, etc.
49
+ */
50
+ atomicUpdate?(path: string, ops: AtomicOp[], sha: string): Promise<FileResult>;
51
+ /**
52
+ * Restore an entry to a previous version identified by `sha`.
53
+ * Only available when `history: true`. Throws `AirdraftError UNSUPPORTED_OPERATION`
54
+ * when called with `history: false`.
55
+ */
56
+ rollback?(path: string, sha: string): Promise<FileResult>;
57
+ /**
58
+ * Tear down internally-owned connections / pools.
59
+ * No-op when the adapter was constructed with a pre-created client.
60
+ */
61
+ abstract close(): Promise<void>;
62
+ protected computeSha(data: unknown): string;
63
+ /**
64
+ * Parse a content path into its collection and slug components.
65
+ * "products/iphone-16.json" → { collection: "products", slug: "iphone-16", ext: ".json" }
66
+ */
67
+ protected parsePath(path: string): {
68
+ collection: string;
69
+ slug: string;
70
+ ext: string;
71
+ };
72
+ }
73
+ //# sourceMappingURL=BaseDatabaseAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BaseDatabaseAdapter.d.ts","sourceRoot":"","sources":["../src/BaseDatabaseAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,WAAW,EACZ,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAEhF;;;;;;;;GAQG;AACH,8BAAsB,mBAAoB,YAAW,cAAc;IACjE,SAAS,CAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IACpC,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACnC,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,IAAI,CAAA;gBAEhD,OAAO,GAAE,gBAAqB;IAa1C,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IACvD,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IACnF,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpE;;;;;;;OAOG;IACG,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAQjD;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CACnB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,kBAAkB,CAAC;IAI9B;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAIjC;;;;;;OAMG;IACH,YAAY,CAAC,CACX,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,QAAQ,EAAE,EACf,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,UAAU,CAAC;IAItB;;;;OAIG;IACH,QAAQ,CAAC,CACP,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,UAAU,CAAC;IAItB;;;OAGG;IACH,QAAQ,CAAC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI/B,SAAS,CAAC,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM;IAM3C;;;OAGG;IACH,SAAS,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;CASrF"}
@@ -0,0 +1,60 @@
1
+ import { LruCache } from '@airdraft/core';
2
+ import { createHash } from 'node:crypto';
3
+ /**
4
+ * Abstract base class for all Airdraft database adapters.
5
+ *
6
+ * Implements the full `StorageAdapter` interface so the engine, plugins,
7
+ * and client code see no difference between a file adapter and a DB adapter.
8
+ *
9
+ * Concrete driver classes (PostgresAdapter, SQLiteAdapter, MongooseAdapter)
10
+ * extend this class and implement the abstract methods.
11
+ */
12
+ export class BaseDatabaseAdapter {
13
+ projectId;
14
+ history;
15
+ cache;
16
+ constructor(options = {}) {
17
+ this.projectId = options.projectId ?? 'default';
18
+ this.history = options.history ?? false;
19
+ this.cache = options.cacheTtlMs && options.cacheTtlMs > 0
20
+ ? new LruCache({
21
+ maxSize: options.cacheMaxSize ?? 500,
22
+ ttlMs: options.cacheTtlMs,
23
+ })
24
+ : null;
25
+ }
26
+ // ─── Concrete: list() delegates to queryEntries ───────────────────────────
27
+ /**
28
+ * Returns all entries for the collection identified by the first path
29
+ * segment of `glob`. Slug-level glob patterns are ignored.
30
+ *
31
+ * This method is never on the hot path when a DB adapter is in use —
32
+ * the engine calls `queryEntries()` directly for `listEntries()`.
33
+ * `list()` is only called by plugin infrastructure (audit log, search).
34
+ */
35
+ async list(glob) {
36
+ const collection = glob.split('/')[0];
37
+ const { entries } = await this.queryEntries(collection, {});
38
+ return entries.map(e => ({ path: e.path, sha: e.sha }));
39
+ }
40
+ // ─── Shared utilities ─────────────────────────────────────────────────────
41
+ computeSha(data) {
42
+ return createHash('sha256')
43
+ .update(JSON.stringify(data))
44
+ .digest('hex');
45
+ }
46
+ /**
47
+ * Parse a content path into its collection and slug components.
48
+ * "products/iphone-16.json" → { collection: "products", slug: "iphone-16", ext: ".json" }
49
+ */
50
+ parsePath(path) {
51
+ const parts = path.split('/');
52
+ const collection = parts[0];
53
+ const filename = parts.slice(1).join('/');
54
+ const dotIdx = filename.lastIndexOf('.');
55
+ const slug = dotIdx === -1 ? filename : filename.slice(0, dotIdx);
56
+ const ext = dotIdx === -1 ? '' : filename.slice(dotIdx);
57
+ return { collection, slug, ext };
58
+ }
59
+ }
60
+ //# sourceMappingURL=BaseDatabaseAdapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BaseDatabaseAdapter.js","sourceRoot":"","sources":["../src/BaseDatabaseAdapter.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGxC;;;;;;;;GAQG;AACH,MAAM,OAAgB,mBAAmB;IACpB,SAAS,CAAQ;IACjB,OAAO,CAAS;IAChB,KAAK,CAAoC;IAE5D,YAAY,UAA4B,EAAE;QACxC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,SAAS,CAAA;QAC/C,IAAI,CAAC,OAAO,GAAK,OAAO,CAAC,OAAO,IAAM,KAAK,CAAA;QAC3C,IAAI,CAAC,KAAK,GAAO,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,UAAU,GAAG,CAAC;YAC3D,CAAC,CAAC,IAAI,QAAQ,CAAoB;gBAC9B,OAAO,EAAE,OAAO,CAAC,YAAY,IAAI,GAAG;gBACpC,KAAK,EAAI,OAAO,CAAC,UAAU;aAC5B,CAAC;YACJ,CAAC,CAAC,IAAI,CAAA;IACV,CAAC;IAQD,6EAA6E;IAE7E;;;;;;;OAOG;IACH,KAAK,CAAC,IAAI,CAAC,IAAY;QACrB,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;QACrC,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;QAC3D,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;IACzD,CAAC;IA2DD,6EAA6E;IAEnE,UAAU,CAAC,IAAa;QAChC,OAAO,UAAU,CAAC,QAAQ,CAAC;aACxB,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;aAC5B,MAAM,CAAC,KAAK,CAAC,CAAA;IAClB,CAAC;IAED;;;OAGG;IACO,SAAS,CAAC,IAAY;QAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAC7B,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;QAC3B,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACzC,MAAM,MAAM,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;QACxC,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAA;QACjE,MAAM,GAAG,GAAI,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QACxD,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,EAAE,CAAA;IAClC,CAAC;CACF"}
@@ -0,0 +1,4 @@
1
+ export { BaseDatabaseAdapter } from './BaseDatabaseAdapter.js';
2
+ export type { DbAdapterOptions, AtomicOp, QueryEntriesResult, ListOptions } from './types.js';
3
+ export { ConflictError, StorageUnavailableError, MigrationError, AirdraftError, } from '@airdraft/core';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,YAAY,EAAE,gBAAgB,EAAE,QAAQ,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAC7F,OAAO,EACL,aAAa,EACb,uBAAuB,EACvB,cAAc,EACd,aAAa,GACd,MAAM,gBAAgB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { BaseDatabaseAdapter } from './BaseDatabaseAdapter.js';
2
+ export { ConflictError, StorageUnavailableError, MigrationError, AirdraftError, } from '@airdraft/core';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D,OAAO,EACL,aAAa,EACb,uBAAuB,EACvB,cAAc,EACd,aAAa,GACd,MAAM,gBAAgB,CAAA"}
@@ -0,0 +1,11 @@
1
+ import type { BaseDatabaseAdapter } from '../BaseDatabaseAdapter.js';
2
+ export interface ContractTestOptions {
3
+ /** Called after each test to reset DB state. */
4
+ afterEach?: () => Promise<void>;
5
+ /** Skip tests that require history: true (optional). */
6
+ skipHistory?: boolean;
7
+ /** Skip tests that require atomicUpdate support (optional). */
8
+ skipAtomicUpdate?: boolean;
9
+ }
10
+ export declare function runStorageAdapterContract(adapter: BaseDatabaseAdapter, options?: ContractTestOptions): void;
11
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../src/testing/contract.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAA;AAGpE,MAAM,WAAW,mBAAmB;IAClC,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/B,wDAAwD;IACxD,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,+DAA+D;IAC/D,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,mBAAmB,EAC5B,OAAO,GAAE,mBAAwB,GAChC,IAAI,CA8KN"}
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Shared contract test suite for all Airdraft database adapters.
3
+ *
4
+ * Usage in each driver package:
5
+ * ```ts
6
+ * import { runStorageAdapterContract } from '@airdraft/db-adapter/testing'
7
+ * import { MySQLAdapter } from '../MySQLAdapter.js'
8
+ *
9
+ * const adapter = new MySQLAdapter({ ... })
10
+ * runStorageAdapterContract(adapter, {
11
+ * afterEach: async () => { // truncate tables },
12
+ * })
13
+ * ```
14
+ */
15
+ import { describe, it, expect, beforeAll, afterEach as vitestAfterEach } from 'vitest';
16
+ import { ConflictError } from '@airdraft/core';
17
+ export function runStorageAdapterContract(adapter, options = {}) {
18
+ const COLLECTION = 'test-posts';
19
+ const SLUG = 'hello-world';
20
+ const PATH = `${COLLECTION}/${SLUG}.json`;
21
+ const CONTENT = JSON.stringify({ title: 'Hello World', published: false, stock: 10 }, null, 2);
22
+ beforeAll(async () => {
23
+ await adapter.migrate();
24
+ });
25
+ if (options.afterEach) {
26
+ vitestAfterEach(options.afterEach);
27
+ }
28
+ // ─── migrate() ─────────────────────────────────────────────────────────────
29
+ describe('migrate()', () => {
30
+ it('is idempotent — calling twice does not throw', async () => {
31
+ await expect(adapter.migrate()).resolves.not.toThrow();
32
+ });
33
+ });
34
+ // ─── write() + read() ───────────────────────────────────────────────────────
35
+ describe('write() + read()', () => {
36
+ it('creates an entry and reads it back', async () => {
37
+ await adapter.write(PATH, CONTENT, { message: 'create test' });
38
+ const result = await adapter.read(PATH);
39
+ expect(result).not.toBeNull();
40
+ expect(result.path).toBe(PATH);
41
+ expect(result.sha).toBeTruthy();
42
+ // Content round-trip: re-parsed then re-serialized — may differ in whitespace
43
+ const parsed = JSON.parse(result.content);
44
+ expect(parsed.title).toBe('Hello World');
45
+ });
46
+ it('read() returns null for a missing path', async () => {
47
+ const result = await adapter.read('nonexistent/missing.json');
48
+ expect(result).toBeNull();
49
+ });
50
+ it('write() updates sha on overwrite', async () => {
51
+ await adapter.write(PATH, CONTENT, { message: 'create' });
52
+ const first = await adapter.read(PATH);
53
+ const updated = JSON.stringify({ title: 'Updated', published: true }, null, 2);
54
+ await adapter.write(PATH, updated, { message: 'update', sha: first.sha });
55
+ const second = await adapter.read(PATH);
56
+ expect(second.sha).not.toBe(first.sha);
57
+ expect(JSON.parse(second.content).title).toBe('Updated');
58
+ });
59
+ it('write() throws ConflictError on stale sha', async () => {
60
+ await adapter.write(PATH, CONTENT, { message: 'create' });
61
+ const stale = 'deadbeef'.repeat(8);
62
+ const updated = JSON.stringify({ title: 'Conflict' }, null, 2);
63
+ await expect(adapter.write(PATH, updated, { message: 'conflict', sha: stale })).rejects.toBeInstanceOf(ConflictError);
64
+ });
65
+ });
66
+ // ─── delete() ──────────────────────────────────────────────────────────────
67
+ describe('delete()', () => {
68
+ it('removes the entry — read() returns null afterwards', async () => {
69
+ await adapter.write(PATH, CONTENT, { message: 'create' });
70
+ const { sha } = (await adapter.read(PATH));
71
+ await adapter.delete(PATH, { message: 'delete', sha });
72
+ expect(await adapter.read(PATH)).toBeNull();
73
+ });
74
+ });
75
+ // ─── list() ────────────────────────────────────────────────────────────────
76
+ describe('list()', () => {
77
+ it('returns all entries for a collection', async () => {
78
+ await adapter.write(`${COLLECTION}/a.json`, JSON.stringify({ title: 'A' }), { message: 'a' });
79
+ await adapter.write(`${COLLECTION}/b.json`, JSON.stringify({ title: 'B' }), { message: 'b' });
80
+ const items = await adapter.list(`${COLLECTION}/*`);
81
+ expect(items.length).toBeGreaterThanOrEqual(2);
82
+ const paths = items.map(i => i.path);
83
+ expect(paths).toContain(`${COLLECTION}/a.json`);
84
+ expect(paths).toContain(`${COLLECTION}/b.json`);
85
+ });
86
+ });
87
+ // ─── queryEntries() ────────────────────────────────────────────────────────
88
+ describe('queryEntries()', () => {
89
+ it('returns entries with correct total', async () => {
90
+ await adapter.write(`${COLLECTION}/p1.json`, JSON.stringify({ title: 'P1', published: true }), { message: 'p1' });
91
+ await adapter.write(`${COLLECTION}/p2.json`, JSON.stringify({ title: 'P2', published: false }), { message: 'p2' });
92
+ const result = await adapter.queryEntries(COLLECTION, {});
93
+ expect(result.total).toBeGreaterThanOrEqual(2);
94
+ expect(result.entries.length).toBeGreaterThanOrEqual(2);
95
+ expect(result.entries[0]).toHaveProperty('path');
96
+ expect(result.entries[0]).toHaveProperty('content');
97
+ expect(result.entries[0]).toHaveProperty('sha');
98
+ });
99
+ it('respects limit and offset', async () => {
100
+ for (let i = 0; i < 5; i++) {
101
+ await adapter.write(`${COLLECTION}/item-${i}.json`, JSON.stringify({ title: `Item ${i}` }), { message: `item ${i}` });
102
+ }
103
+ const page1 = await adapter.queryEntries(COLLECTION, { limit: 2, offset: 0 });
104
+ const page2 = await adapter.queryEntries(COLLECTION, { limit: 2, offset: 2 });
105
+ expect(page1.entries.length).toBe(2);
106
+ expect(page2.entries.length).toBe(2);
107
+ expect(page1.entries[0].path).not.toBe(page2.entries[0].path);
108
+ });
109
+ it('total reflects all matching rows regardless of pagination', async () => {
110
+ for (let i = 0; i < 3; i++) {
111
+ await adapter.write(`${COLLECTION}/tot-${i}.json`, JSON.stringify({ title: `T${i}` }), { message: `t${i}` });
112
+ }
113
+ const result = await adapter.queryEntries(COLLECTION, { limit: 1, offset: 0 });
114
+ expect(result.total).toBeGreaterThan(1);
115
+ expect(result.entries.length).toBe(1);
116
+ });
117
+ });
118
+ // ─── Serialization round-trips ─────────────────────────────────────────────
119
+ describe('serialization round-trips', () => {
120
+ it('JSON: round-trips correctly', async () => {
121
+ const data = { title: 'JSON test', count: 42, tags: ['a', 'b'], active: true };
122
+ await adapter.write('rt/json.json', JSON.stringify(data, null, 2), { message: 'rt-json' });
123
+ const result = await adapter.read('rt/json.json');
124
+ const parsed = JSON.parse(result.content);
125
+ expect(parsed).toMatchObject(data);
126
+ });
127
+ it('YAML: round-trips correctly', async () => {
128
+ const yamlContent = 'title: YAML test\ncount: 7\npublished: true';
129
+ await adapter.write('rt/yaml.yaml', yamlContent, { message: 'rt-yaml' });
130
+ const result = await adapter.read('rt/yaml.yaml');
131
+ expect(result.content).toBeTruthy();
132
+ });
133
+ it('MDX: round-trips body correctly', async () => {
134
+ const mdxContent = '---\ntitle: MDX test\n---\n\nHello **world**.';
135
+ await adapter.write('rt/post.mdx', mdxContent, { message: 'rt-mdx' });
136
+ const result = await adapter.read('rt/post.mdx');
137
+ expect(result.content).toContain('Hello **world**.');
138
+ });
139
+ });
140
+ // ─── atomicUpdate() ────────────────────────────────────────────────────────
141
+ if (!options.skipAtomicUpdate && typeof adapter.atomicUpdate === 'function') {
142
+ describe('atomicUpdate()', () => {
143
+ it('increments a numeric field atomically', async () => {
144
+ await adapter.write(PATH, JSON.stringify({ stock: 10 }, null, 2), { message: 'create' });
145
+ const { sha } = (await adapter.read(PATH));
146
+ const updated = await adapter.atomicUpdate(PATH, [{ op: 'increment', path: 'stock', by: -3 }], sha);
147
+ const parsed = JSON.parse(updated.content);
148
+ expect(parsed.stock).toBe(7);
149
+ });
150
+ it('set changes a field value', async () => {
151
+ await adapter.write(PATH, JSON.stringify({ status: 'pending' }, null, 2), { message: 'create' });
152
+ const { sha } = (await adapter.read(PATH));
153
+ const updated = await adapter.atomicUpdate(PATH, [{ op: 'set', path: 'status', value: 'shipped' }], sha);
154
+ expect(JSON.parse(updated.content).status).toBe('shipped');
155
+ });
156
+ it('throws ConflictError on stale sha', async () => {
157
+ await adapter.write(PATH, JSON.stringify({ stock: 5 }, null, 2), { message: 'create' });
158
+ await expect(adapter.atomicUpdate(PATH, [{ op: 'increment', path: 'stock', by: 1 }], 'stale'.repeat(12))).rejects.toBeInstanceOf(ConflictError);
159
+ });
160
+ });
161
+ }
162
+ }
163
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/testing/contract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ import type { ListOptions } from '@airdraft/core';
2
+ export interface DbAdapterOptions {
3
+ /**
4
+ * Isolates all reads/writes to this project within a shared DB instance.
5
+ * Defaults to `'default'` for single-project self-hosted deployments.
6
+ */
7
+ projectId?: string;
8
+ /**
9
+ * When true, every write is mirrored to `airdraft_entry_history`.
10
+ * Requires a migration that creates the history table.
11
+ * MongoDB: also requires a replica set.
12
+ * @default false
13
+ */
14
+ history?: boolean;
15
+ /**
16
+ * Read cache TTL in milliseconds. Set to 0 to disable.
17
+ * @default 0
18
+ */
19
+ cacheTtlMs?: number;
20
+ /**
21
+ * Maximum number of entries in the read cache.
22
+ * @default 500
23
+ */
24
+ cacheMaxSize?: number;
25
+ }
26
+ export interface QueryEntriesResult {
27
+ entries: Array<{
28
+ path: string;
29
+ content: string;
30
+ sha: string;
31
+ }>;
32
+ /** Total matching rows before pagination — used for meta.total */
33
+ total: number;
34
+ }
35
+ export type AtomicOp = {
36
+ op: 'increment';
37
+ path: string;
38
+ by: number;
39
+ } | {
40
+ op: 'set';
41
+ path: string;
42
+ value: unknown;
43
+ } | {
44
+ op: 'push';
45
+ path: string;
46
+ value: unknown;
47
+ } | {
48
+ op: 'pull';
49
+ path: string;
50
+ value: unknown;
51
+ };
52
+ export type { ListOptions };
53
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAMjD,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAMD,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,KAAK,CAAC;QACb,IAAI,EAAE,MAAM,CAAA;QACZ,OAAO,EAAE,MAAM,CAAA;QACf,GAAG,EAAE,MAAM,CAAA;KACZ,CAAC,CAAA;IACF,kEAAkE;IAClE,KAAK,EAAE,MAAM,CAAA;CACd;AAMD,MAAM,MAAM,QAAQ,GAChB;IAAE,EAAE,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC7C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAO,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACjD;IAAE,EAAE,EAAE,MAAM,CAAC;IAAM,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACjD;IAAE,EAAE,EAAE,MAAM,CAAC;IAAM,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAA;AAGrD,YAAY,EAAE,WAAW,EAAE,CAAA"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ // @airdraft/db-adapter — shared types
2
+ export {};
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,sCAAsC"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@airdraft/db-adapter",
3
+ "version": "0.1.0",
4
+ "description": "Airdraft base database adapter — abstract class, shared types, and contract test suite",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./testing": {
14
+ "import": "./dist/testing/contract.js",
15
+ "types": "./dist/testing/contract.d.ts"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "dev": "tsc --watch",
25
+ "clean": "rm -rf dist",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "typecheck": "tsc --noEmit",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "license": "MIT",
35
+ "dependencies": {
36
+ "@airdraft/core": "workspace:*"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^20.0.0",
40
+ "typescript": "^5.4.0",
41
+ "vitest": "^2.0.0"
42
+ },
43
+ "peerDependenciesMeta": {}
44
+ }