@tallyui/database 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.
@@ -0,0 +1,65 @@
1
+ import { RxDatabase, RxCollection, RxStorage } from 'rxdb';
2
+ import { TallyConnector, ReplicationAdapter, SyncContext } from '@tallyui/core';
3
+ import { RxReplicationState } from 'rxdb/plugins/replication';
4
+
5
+ /**
6
+ * The shape of a Tally database — keyed by collection name.
7
+ */
8
+ type TallyDatabase = RxDatabase<{
9
+ [key: string]: RxCollection;
10
+ }>;
11
+ interface CreateDatabaseOptions {
12
+ /** The connector whose schemas define the collections */
13
+ connector: TallyConnector;
14
+ /** Database name (defaults to `tally_${connector.id}`) */
15
+ name?: string;
16
+ /** RxDB storage adapter (defaults to in-memory for dev/demo) */
17
+ storage?: any;
18
+ }
19
+ /**
20
+ * Create an RxDB database from a connector's schemas.
21
+ *
22
+ * This is the main entry point — give it a connector and it builds
23
+ * the database with the right collections and schemas.
24
+ *
25
+ * ```ts
26
+ * import { woocommerceConnector } from '@tallyui/connector-woocommerce';
27
+ * import { createTallyDatabase } from '@tallyui/database';
28
+ *
29
+ * const db = await createTallyDatabase({ connector: woocommerceConnector });
30
+ * const products = await db.products.find().exec();
31
+ * ```
32
+ */
33
+ declare function createTallyDatabase(options: CreateDatabaseOptions): Promise<TallyDatabase>;
34
+
35
+ /**
36
+ * Returns the appropriate RxDB storage adapter for the current platform.
37
+ *
38
+ * - Web browser: Dexie (IndexedDB)
39
+ * - React Native: throws with guidance to provide SQLite storage explicitly
40
+ * - Node/SSR/test: In-memory
41
+ *
42
+ * Override by passing a storage directly to createTallyDatabase().
43
+ */
44
+ declare function getStorage(): RxStorage<any, any>;
45
+
46
+ interface StartReplicationOptions<RxDocType, CheckpointType = any> {
47
+ collection: RxCollection<RxDocType>;
48
+ adapter: ReplicationAdapter<RxDocType, CheckpointType>;
49
+ context: SyncContext;
50
+ /** Keep syncing after initial pull (default: true) */
51
+ live?: boolean;
52
+ /** Retry interval in ms on error (default: 5000) */
53
+ retryTime?: number;
54
+ /** Start immediately (default: true) */
55
+ autoStart?: boolean;
56
+ }
57
+ /**
58
+ * Start RxDB replication for a single collection using a ReplicationAdapter.
59
+ *
60
+ * Returns the RxReplicationState which exposes observables for monitoring
61
+ * and methods like cancel(), awaitInSync(), reSync().
62
+ */
63
+ declare function startReplication<RxDocType, CheckpointType = any>({ collection, adapter, context, live, retryTime, autoStart, }: StartReplicationOptions<RxDocType, CheckpointType>): RxReplicationState<RxDocType, CheckpointType>;
64
+
65
+ export { type CreateDatabaseOptions, type StartReplicationOptions, type TallyDatabase, createTallyDatabase, getStorage, startReplication };
package/dist/index.js ADDED
@@ -0,0 +1,80 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/create-db.ts
9
+ import { createRxDatabase, addRxPlugin } from "rxdb";
10
+ import { RxDBDevModePlugin } from "rxdb/plugins/dev-mode";
11
+ import { getRxStorageMemory } from "rxdb/plugins/storage-memory";
12
+ if (process.env.NODE_ENV !== "production") {
13
+ addRxPlugin(RxDBDevModePlugin);
14
+ }
15
+ async function createTallyDatabase(options) {
16
+ const {
17
+ connector,
18
+ name = `tally_${connector.id}`,
19
+ storage = getRxStorageMemory()
20
+ } = options;
21
+ const db = await createRxDatabase({
22
+ name,
23
+ storage,
24
+ multiInstance: false,
25
+ ignoreDuplicate: true
26
+ });
27
+ const collectionConfigs = {};
28
+ for (const [collectionName, schema] of Object.entries(connector.schemas)) {
29
+ collectionConfigs[collectionName] = { schema };
30
+ }
31
+ await db.addCollections(collectionConfigs);
32
+ return db;
33
+ }
34
+
35
+ // src/storage.ts
36
+ import { getRxStorageMemory as getRxStorageMemory2 } from "rxdb/plugins/storage-memory";
37
+ function getStorage() {
38
+ if (typeof window === "undefined") {
39
+ return getRxStorageMemory2();
40
+ }
41
+ if (typeof navigator !== "undefined" && navigator.product === "ReactNative") {
42
+ throw new Error(
43
+ 'React Native detected. Pass a storage adapter explicitly:\n import { getRxStorageSQLite } from "@tallyui/storage-sqlite";\n createTallyDatabase({ connector, storage: getRxStorageSQLite({ database }) })'
44
+ );
45
+ }
46
+ const { getRxStorageDexie } = __require("rxdb/plugins/storage-dexie");
47
+ return getRxStorageDexie();
48
+ }
49
+
50
+ // src/replication.ts
51
+ import { replicateRxCollection } from "rxdb/plugins/replication";
52
+ function startReplication({
53
+ collection,
54
+ adapter,
55
+ context,
56
+ live = true,
57
+ retryTime = 5e3,
58
+ autoStart = true
59
+ }) {
60
+ return replicateRxCollection({
61
+ collection,
62
+ replicationIdentifier: `${context.connectorId}-${collection.name}`,
63
+ pull: {
64
+ handler: (checkpoint, batchSize) => adapter.pull.handler(checkpoint, batchSize, context),
65
+ stream$: adapter.pull.stream$
66
+ },
67
+ push: adapter.push ? {
68
+ handler: (rows) => adapter.push.handler(rows, context),
69
+ batchSize: adapter.push.batchSize
70
+ } : void 0,
71
+ live,
72
+ retryTime,
73
+ autoStart
74
+ });
75
+ }
76
+ export {
77
+ createTallyDatabase,
78
+ getStorage,
79
+ startReplication
80
+ };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@tallyui/database",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "RxDB database factory for Tally UI — accepts pluggable connector schemas",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "source": "./src/index.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "source": "./src/index.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src"
19
+ ],
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "rxdb": "16.21.1",
23
+ "rxjs": "7.8.2"
24
+ },
25
+ "peerDependencies": {
26
+ "@tallyui/core": "0.2.0"
27
+ },
28
+ "scripts": {
29
+ "build": "tsup",
30
+ "typecheck": "tsc --noEmit",
31
+ "test": "echo \"no tests yet\""
32
+ }
33
+ }
@@ -0,0 +1,65 @@
1
+ import { createRxDatabase, addRxPlugin, type RxDatabase, type RxCollection } from 'rxdb';
2
+ import { RxDBDevModePlugin } from 'rxdb/plugins/dev-mode';
3
+ import { getRxStorageMemory } from 'rxdb/plugins/storage-memory';
4
+
5
+ import type { TallyConnector } from '@tallyui/core';
6
+
7
+ // Enable dev mode in non-production
8
+ if (process.env.NODE_ENV !== 'production') {
9
+ addRxPlugin(RxDBDevModePlugin);
10
+ }
11
+
12
+ /**
13
+ * The shape of a Tally database — keyed by collection name.
14
+ */
15
+ export type TallyDatabase = RxDatabase<{
16
+ [key: string]: RxCollection;
17
+ }>;
18
+
19
+ export interface CreateDatabaseOptions {
20
+ /** The connector whose schemas define the collections */
21
+ connector: TallyConnector;
22
+ /** Database name (defaults to `tally_${connector.id}`) */
23
+ name?: string;
24
+ /** RxDB storage adapter (defaults to in-memory for dev/demo) */
25
+ storage?: any;
26
+ }
27
+
28
+ /**
29
+ * Create an RxDB database from a connector's schemas.
30
+ *
31
+ * This is the main entry point — give it a connector and it builds
32
+ * the database with the right collections and schemas.
33
+ *
34
+ * ```ts
35
+ * import { woocommerceConnector } from '@tallyui/connector-woocommerce';
36
+ * import { createTallyDatabase } from '@tallyui/database';
37
+ *
38
+ * const db = await createTallyDatabase({ connector: woocommerceConnector });
39
+ * const products = await db.products.find().exec();
40
+ * ```
41
+ */
42
+ export async function createTallyDatabase(options: CreateDatabaseOptions): Promise<TallyDatabase> {
43
+ const {
44
+ connector,
45
+ name = `tally_${connector.id}`,
46
+ storage = getRxStorageMemory(),
47
+ } = options;
48
+
49
+ const db = await createRxDatabase({
50
+ name,
51
+ storage,
52
+ multiInstance: false,
53
+ ignoreDuplicate: true,
54
+ });
55
+
56
+ // Build collection configs from connector schemas
57
+ const collectionConfigs: Record<string, { schema: any }> = {};
58
+ for (const [collectionName, schema] of Object.entries(connector.schemas)) {
59
+ collectionConfigs[collectionName] = { schema };
60
+ }
61
+
62
+ await db.addCollections(collectionConfigs);
63
+
64
+ return db as TallyDatabase;
65
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export { createTallyDatabase } from './create-db';
2
+ export type { TallyDatabase, CreateDatabaseOptions } from './create-db';
3
+
4
+ export { getStorage } from './storage';
5
+
6
+ export { startReplication } from './replication';
7
+ export type { StartReplicationOptions } from './replication';
@@ -0,0 +1,97 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { createRxDatabase, addRxPlugin } from 'rxdb';
3
+ import { RxDBDevModePlugin } from 'rxdb/plugins/dev-mode';
4
+ import { getRxStorageMemory } from 'rxdb/plugins/storage-memory';
5
+ import { wrappedValidateAjvStorage } from 'rxdb/plugins/validate-ajv';
6
+
7
+ import { startReplication } from './replication';
8
+ import type { ReplicationAdapter, SyncContext } from '@tallyui/core';
9
+
10
+ addRxPlugin(RxDBDevModePlugin);
11
+
12
+ const storage = wrappedValidateAjvStorage({ storage: getRxStorageMemory() });
13
+
14
+ const testSchema = {
15
+ version: 0,
16
+ primaryKey: 'id',
17
+ type: 'object' as const,
18
+ properties: {
19
+ id: { type: 'string', maxLength: 100 },
20
+ name: { type: 'string' },
21
+ },
22
+ required: ['id', 'name'],
23
+ };
24
+
25
+ const context: SyncContext = {
26
+ connectorId: 'test',
27
+ baseUrl: 'https://example.com',
28
+ headers: {},
29
+ };
30
+
31
+ describe('startReplication', () => {
32
+ let db: any;
33
+
34
+ beforeEach(async () => {
35
+ db = await createRxDatabase({
36
+ name: `test_${Date.now()}`,
37
+ storage,
38
+ multiInstance: false,
39
+ ignoreDuplicate: true,
40
+ });
41
+ await db.addCollections({ products: { schema: testSchema } });
42
+ });
43
+
44
+ afterEach(async () => {
45
+ await db?.close();
46
+ });
47
+
48
+ it('starts replication and pulls documents', async () => {
49
+ const adapter: ReplicationAdapter<any, any> = {
50
+ pull: {
51
+ handler: vi.fn().mockResolvedValueOnce({
52
+ documents: [
53
+ { id: '1', name: 'Widget', _deleted: false },
54
+ ],
55
+ checkpoint: { id: '1' },
56
+ }).mockResolvedValue({
57
+ documents: [],
58
+ checkpoint: { id: '1' },
59
+ }),
60
+ },
61
+ };
62
+
63
+ const state = startReplication({
64
+ collection: db.products,
65
+ adapter,
66
+ context,
67
+ });
68
+
69
+ await state.awaitInSync();
70
+
71
+ const docs = await db.products.find().exec();
72
+ expect(docs).toHaveLength(1);
73
+ expect(docs[0].name).toBe('Widget');
74
+
75
+ await state.cancel();
76
+ });
77
+
78
+ it('returns RxReplicationState with observables', async () => {
79
+ const adapter: ReplicationAdapter<any, any> = {
80
+ pull: {
81
+ handler: vi.fn().mockResolvedValue({ documents: [], checkpoint: {} }),
82
+ },
83
+ };
84
+
85
+ const state = startReplication({
86
+ collection: db.products,
87
+ adapter,
88
+ context,
89
+ });
90
+
91
+ expect(state.error$).toBeDefined();
92
+ expect(state.active$).toBeDefined();
93
+ expect(state.received$).toBeDefined();
94
+
95
+ await state.cancel();
96
+ });
97
+ });
@@ -0,0 +1,50 @@
1
+ import { replicateRxCollection, type RxReplicationState } from 'rxdb/plugins/replication';
2
+ import type { RxCollection } from 'rxdb';
3
+
4
+ import type { ReplicationAdapter, SyncContext } from '@tallyui/core';
5
+
6
+ export interface StartReplicationOptions<RxDocType, CheckpointType = any> {
7
+ collection: RxCollection<RxDocType>;
8
+ adapter: ReplicationAdapter<RxDocType, CheckpointType>;
9
+ context: SyncContext;
10
+ /** Keep syncing after initial pull (default: true) */
11
+ live?: boolean;
12
+ /** Retry interval in ms on error (default: 5000) */
13
+ retryTime?: number;
14
+ /** Start immediately (default: true) */
15
+ autoStart?: boolean;
16
+ }
17
+
18
+ /**
19
+ * Start RxDB replication for a single collection using a ReplicationAdapter.
20
+ *
21
+ * Returns the RxReplicationState which exposes observables for monitoring
22
+ * and methods like cancel(), awaitInSync(), reSync().
23
+ */
24
+ export function startReplication<RxDocType, CheckpointType = any>({
25
+ collection,
26
+ adapter,
27
+ context,
28
+ live = true,
29
+ retryTime = 5000,
30
+ autoStart = true,
31
+ }: StartReplicationOptions<RxDocType, CheckpointType>): RxReplicationState<RxDocType, CheckpointType> {
32
+ return replicateRxCollection({
33
+ collection,
34
+ replicationIdentifier: `${context.connectorId}-${collection.name}`,
35
+ pull: {
36
+ handler: (checkpoint, batchSize) =>
37
+ adapter.pull.handler(checkpoint as CheckpointType | undefined, batchSize, context),
38
+ stream$: adapter.pull.stream$,
39
+ },
40
+ push: adapter.push
41
+ ? {
42
+ handler: (rows) => adapter.push!.handler(rows, context),
43
+ batchSize: adapter.push.batchSize,
44
+ }
45
+ : undefined,
46
+ live,
47
+ retryTime,
48
+ autoStart,
49
+ });
50
+ }
@@ -0,0 +1,19 @@
1
+ // @vitest-environment node
2
+ import { describe, it, expect } from 'vitest';
3
+
4
+ import { getStorage } from './storage';
5
+
6
+ describe('getStorage', () => {
7
+ it('returns a storage object with a name property', () => {
8
+ const storage = getStorage();
9
+ expect(storage).toBeDefined();
10
+ expect(storage.name).toBeDefined();
11
+ expect(typeof storage.createStorageInstance).toBe('function');
12
+ });
13
+
14
+ it('returns memory storage in node/test environment', () => {
15
+ const storage = getStorage();
16
+ // In Node.js (test env), we expect memory storage
17
+ expect(storage.name).toBe('memory');
18
+ });
19
+ });
package/src/storage.ts ADDED
@@ -0,0 +1,33 @@
1
+ import type { RxStorage } from 'rxdb';
2
+ import { getRxStorageMemory } from 'rxdb/plugins/storage-memory';
3
+
4
+ /**
5
+ * Returns the appropriate RxDB storage adapter for the current platform.
6
+ *
7
+ * - Web browser: Dexie (IndexedDB)
8
+ * - React Native: throws with guidance to provide SQLite storage explicitly
9
+ * - Node/SSR/test: In-memory
10
+ *
11
+ * Override by passing a storage directly to createTallyDatabase().
12
+ */
13
+ export function getStorage(): RxStorage<any, any> {
14
+ // Node.js / SSR / test — no persistent storage needed
15
+ if (typeof window === 'undefined') {
16
+ return getRxStorageMemory();
17
+ }
18
+
19
+ // React Native — no IndexedDB available.
20
+ // Users must provide SQLite storage explicitly via createTallyDatabase({ storage }).
21
+ if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
22
+ throw new Error(
23
+ 'React Native detected. Pass a storage adapter explicitly:\n' +
24
+ ' import { getRxStorageSQLite } from "@tallyui/storage-sqlite";\n' +
25
+ ' createTallyDatabase({ connector, storage: getRxStorageSQLite({ database }) })'
26
+ );
27
+ }
28
+
29
+ // Web browser — use Dexie (IndexedDB)
30
+ // Dynamic import avoided here; Dexie is bundled with rxdb.
31
+ const { getRxStorageDexie } = require('rxdb/plugins/storage-dexie');
32
+ return getRxStorageDexie();
33
+ }