@tallyui/core 0.2.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,237 @@
1
+ import { RxReplicationPullStreamItem, RxReplicationWriteToMasterRow, RxJsonSchema } from 'rxdb';
2
+ import { Observable } from 'rxjs';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
4
+ import { ReactNode } from 'react';
5
+
6
+ /**
7
+ * Product traits — the stable interface that UI components program against.
8
+ *
9
+ * Each connector implements these accessors to extract standard product data
10
+ * from its own schema shape. Components call these instead of reaching into
11
+ * the raw document, so they work identically across WooCommerce, Medusa,
12
+ * Vendure, Shopify, etc.
13
+ *
14
+ * The generic `Doc` type is the connector's raw RxDB document type.
15
+ */
16
+ interface ProductTraits<Doc = any> {
17
+ /** Product display name */
18
+ getName: (doc: Doc) => string;
19
+ /** SKU / stock keeping unit */
20
+ getSku: (doc: Doc) => string | undefined;
21
+ /** Current selling price as a string (connector-native format) */
22
+ getPrice: (doc: Doc) => string | undefined;
23
+ /** Regular (non-sale) price */
24
+ getRegularPrice: (doc: Doc) => string | undefined;
25
+ /** Sale price, if on sale */
26
+ getSalePrice: (doc: Doc) => string | undefined;
27
+ /** Whether the product is currently on sale */
28
+ isOnSale: (doc: Doc) => boolean;
29
+ /** Primary image URL */
30
+ getImageUrl: (doc: Doc) => string | undefined;
31
+ /** All image URLs */
32
+ getImageUrls: (doc: Doc) => string[];
33
+ /** Short description / excerpt */
34
+ getDescription: (doc: Doc) => string | undefined;
35
+ /** Stock status */
36
+ getStockStatus: (doc: Doc) => 'instock' | 'outofstock' | 'onbackorder' | 'unknown';
37
+ /** Stock quantity (null if not tracked) */
38
+ getStockQuantity: (doc: Doc) => number | null;
39
+ /** Whether this product has variants/variations */
40
+ hasVariants: (doc: Doc) => boolean;
41
+ /** Product type (simple, variable, etc. — connector-specific but useful for UI hints) */
42
+ getType: (doc: Doc) => string;
43
+ /** Barcode / UPC / EAN */
44
+ getBarcode: (doc: Doc) => string | undefined;
45
+ /** Categories as simple label strings */
46
+ getCategoryNames: (doc: Doc) => string[];
47
+ /** The connector-specific unique ID (as string for consistency) */
48
+ getId: (doc: Doc) => string;
49
+ }
50
+
51
+ /**
52
+ * Customer traits — the stable interface that UI components program against.
53
+ *
54
+ * Each connector implements these to extract standard customer data
55
+ * from its own schema shape.
56
+ */
57
+ interface CustomerTraits<Doc = any> {
58
+ /** Customer display name (first + last or company) */
59
+ getName: (doc: Doc) => string;
60
+ /** Email address */
61
+ getEmail: (doc: Doc) => string | undefined;
62
+ /** Phone number */
63
+ getPhone: (doc: Doc) => string | undefined;
64
+ /** One-line address summary (e.g., "123 Main St, Springfield") */
65
+ getAddressSummary: (doc: Doc) => string | undefined;
66
+ /** The connector-specific unique ID (as string for consistency) */
67
+ getId: (doc: Doc) => string;
68
+ }
69
+
70
+ /**
71
+ * Replication adapter interface for a single collection.
72
+ *
73
+ * Each connector implements this to provide pull/push handlers
74
+ * compatible with RxDB's replicateRxCollection protocol.
75
+ *
76
+ * CheckpointType is connector-specific (cursor, offset+timestamp, etc).
77
+ */
78
+ interface ReplicationAdapter<RxDocType, CheckpointType = any> {
79
+ pull: {
80
+ /**
81
+ * Fetch documents changed since the last checkpoint.
82
+ * Return an empty documents array when fully caught up.
83
+ */
84
+ handler: (lastCheckpoint: CheckpointType | undefined, batchSize: number, context: SyncContext) => Promise<{
85
+ documents: Array<RxDocType & {
86
+ _deleted: boolean;
87
+ }>;
88
+ checkpoint: CheckpointType;
89
+ }>;
90
+ /**
91
+ * Optional real-time event stream.
92
+ * Emit { documents, checkpoint } for live updates,
93
+ * or 'RESYNC' to trigger a full pull cycle.
94
+ */
95
+ stream$?: Observable<RxReplicationPullStreamItem<RxDocType, CheckpointType>>;
96
+ };
97
+ push?: {
98
+ /**
99
+ * Push local changes to the remote API.
100
+ * Return an array of conflict documents (server's version wins).
101
+ * Return empty array if no conflicts.
102
+ */
103
+ handler: (changeRows: RxReplicationWriteToMasterRow<RxDocType>[], context: SyncContext) => Promise<Array<RxDocType & {
104
+ _deleted: boolean;
105
+ }>>;
106
+ /** Max documents per push batch. Defaults to 100. */
107
+ batchSize?: number;
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Authentication configuration for a connector.
113
+ * Each API has wildly different auth — JWT, API keys, OAuth, etc.
114
+ * The connector declares what it needs and how to use it.
115
+ */
116
+ interface ConnectorAuth {
117
+ /** Human-readable auth type for UI display */
118
+ type: string;
119
+ /** Fields required from the user to authenticate */
120
+ fields: AuthField[];
121
+ /** Build request headers from stored credentials */
122
+ getHeaders: (credentials: Record<string, string>) => Record<string, string>;
123
+ /** Optional: validate that credentials work (e.g., test API call) */
124
+ validate?: (credentials: Record<string, string>) => Promise<boolean>;
125
+ }
126
+ interface AuthField {
127
+ key: string;
128
+ label: string;
129
+ type: 'text' | 'password' | 'url';
130
+ placeholder?: string;
131
+ required?: boolean;
132
+ }
133
+ /**
134
+ * Sync configuration for a collection.
135
+ * Defines how to fetch data from the remote API and push changes back.
136
+ *
137
+ * @deprecated Use ReplicationAdapter instead. This interface will be removed
138
+ * once all connectors have migrated to the RxDB replication protocol.
139
+ */
140
+ interface CollectionSync<T = any> {
141
+ /** Fetch all remote IDs (for diffing against local) */
142
+ fetchAllIds: (context: SyncContext) => Promise<RemoteIdEntry[]>;
143
+ /** Fetch documents by IDs */
144
+ fetchByIds: (ids: string[], context: SyncContext) => Promise<T[]>;
145
+ /** Fetch documents modified after a given date */
146
+ fetchModifiedAfter?: (date: string, context: SyncContext) => Promise<T[]>;
147
+ /** Push a local change to the remote */
148
+ push?: (doc: T, context: SyncContext) => Promise<T>;
149
+ /** Create a new document on the remote */
150
+ create?: (doc: Partial<T>, context: SyncContext) => Promise<T>;
151
+ /** Delete a document on the remote */
152
+ delete?: (id: string, context: SyncContext) => Promise<void>;
153
+ }
154
+ interface RemoteIdEntry {
155
+ id: string;
156
+ dateModified?: string;
157
+ }
158
+ interface SyncContext {
159
+ /** Unique connector identifier (used as replication namespace) */
160
+ connectorId: string;
161
+ /** Base URL for the API */
162
+ baseUrl: string;
163
+ /** Authenticated headers */
164
+ headers: Record<string, string>;
165
+ /** Optional abort signal */
166
+ signal?: AbortSignal;
167
+ }
168
+ /**
169
+ * Schema definitions for a connector.
170
+ * Each connector provides its own RxDB schemas that mirror its API shape.
171
+ */
172
+ interface ConnectorSchemas {
173
+ products: RxJsonSchema<any>;
174
+ [key: string]: RxJsonSchema<any>;
175
+ }
176
+ /**
177
+ * Trait implementations for a connector.
178
+ * These are the accessor functions that components program against.
179
+ */
180
+ interface ConnectorTraits {
181
+ product: ProductTraits;
182
+ customer?: CustomerTraits;
183
+ }
184
+ /**
185
+ * The main connector interface.
186
+ * Each backend (WooCommerce, Medusa, Vendure, etc.) implements this.
187
+ */
188
+ interface TallyConnector {
189
+ /** Unique identifier for this connector */
190
+ id: string;
191
+ /** Human-readable name */
192
+ name: string;
193
+ /** Description of the backend this connects to */
194
+ description: string;
195
+ /** Icon or logo URL */
196
+ icon?: string;
197
+ /** Authentication configuration */
198
+ auth: ConnectorAuth;
199
+ /** RxDB schemas for each collection */
200
+ schemas: ConnectorSchemas;
201
+ /** Trait implementations — how to extract standard data from connector-specific docs */
202
+ traits: ConnectorTraits;
203
+ /**
204
+ * @deprecated Use `replication` instead.
205
+ * Sync configuration for each collection (legacy pull-based sync).
206
+ */
207
+ sync: {
208
+ products: CollectionSync;
209
+ [key: string]: CollectionSync;
210
+ };
211
+ /** Replication adapters for each collection (RxDB replication protocol) */
212
+ replication?: {
213
+ products?: ReplicationAdapter<any>;
214
+ [key: string]: ReplicationAdapter<any> | undefined;
215
+ };
216
+ }
217
+
218
+ interface ConnectorProviderProps {
219
+ connector: TallyConnector;
220
+ children: ReactNode;
221
+ }
222
+ declare function ConnectorProvider({ connector, children }: ConnectorProviderProps): react_jsx_runtime.JSX.Element;
223
+ /**
224
+ * Access the active connector. Throws if used outside a ConnectorProvider.
225
+ */
226
+ declare function useConnector(): TallyConnector;
227
+ /**
228
+ * Convenience hook: grab just the product traits from the active connector.
229
+ */
230
+ declare function useProductTraits(): ProductTraits<any>;
231
+ /**
232
+ * Convenience hook: grab just the customer traits from the active connector.
233
+ * Returns undefined if the connector doesn't implement customer traits.
234
+ */
235
+ declare function useCustomerTraits(): CustomerTraits<any> | undefined;
236
+
237
+ export { type AuthField, type CollectionSync, type ConnectorAuth, ConnectorProvider, type ConnectorProviderProps, type ConnectorSchemas, type ConnectorTraits, type CustomerTraits, type ProductTraits, type RemoteIdEntry, type ReplicationAdapter, type SyncContext, type TallyConnector, useConnector, useCustomerTraits, useProductTraits };
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ // src/context/connector-context.tsx
2
+ import { createContext, useContext } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ var ConnectorContext = createContext(null);
5
+ function ConnectorProvider({ connector, children }) {
6
+ return /* @__PURE__ */ jsx(ConnectorContext.Provider, { value: connector, children });
7
+ }
8
+ function useConnector() {
9
+ const connector = useContext(ConnectorContext);
10
+ if (!connector) {
11
+ throw new Error(
12
+ "useConnector() must be used within a <ConnectorProvider>. Wrap your app with <ConnectorProvider connector={yourConnector}>."
13
+ );
14
+ }
15
+ return connector;
16
+ }
17
+ function useProductTraits() {
18
+ const connector = useConnector();
19
+ return connector.traits.product;
20
+ }
21
+ function useCustomerTraits() {
22
+ const connector = useConnector();
23
+ return connector.traits.customer;
24
+ }
25
+ export {
26
+ ConnectorProvider,
27
+ useConnector,
28
+ useCustomerTraits,
29
+ useProductTraits
30
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@tallyui/core",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "Core interfaces, traits, and context providers for Tally UI",
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
+ "peerDependencies": {
22
+ "react": ">=18",
23
+ "rxdb": ">=16",
24
+ "rxjs": ">=7"
25
+ },
26
+ "devDependencies": {
27
+ "@types/react": ">=18"
28
+ },
29
+ "scripts": {
30
+ "build": "tsup",
31
+ "typecheck": "tsc --noEmit",
32
+ "test": "echo \"no tests yet\""
33
+ }
34
+ }
@@ -0,0 +1,100 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { renderHook } from '@testing-library/react';
3
+ import type { ReactNode } from 'react';
4
+ import { ConnectorProvider, useConnector, useProductTraits } from '../context/connector-context';
5
+ import type { TallyConnector } from '../types';
6
+
7
+ /**
8
+ * Minimal connector stub that satisfies the TallyConnector interface
9
+ * just enough for context tests.
10
+ */
11
+ const stubConnector: TallyConnector = {
12
+ id: 'test',
13
+ name: 'Test Connector',
14
+ description: 'Stub for testing',
15
+ auth: {
16
+ type: 'none',
17
+ fields: [],
18
+ getHeaders: () => ({}),
19
+ },
20
+ schemas: {
21
+ products: {
22
+ version: 0,
23
+ primaryKey: 'id',
24
+ type: 'object',
25
+ properties: { id: { type: 'string', maxLength: 100 } },
26
+ required: ['id'],
27
+ },
28
+ },
29
+ traits: {
30
+ product: {
31
+ getId: (doc) => doc.id,
32
+ getName: (doc) => doc.name,
33
+ getSku: () => undefined,
34
+ getPrice: () => undefined,
35
+ getRegularPrice: () => undefined,
36
+ getSalePrice: () => undefined,
37
+ isOnSale: () => false,
38
+ getImageUrl: () => undefined,
39
+ getImageUrls: () => [],
40
+ getDescription: () => undefined,
41
+ getStockStatus: () => 'unknown',
42
+ getStockQuantity: () => null,
43
+ hasVariants: () => false,
44
+ getType: () => 'simple',
45
+ getBarcode: () => undefined,
46
+ getCategoryNames: () => [],
47
+ },
48
+ },
49
+ sync: {
50
+ products: {
51
+ fetchAllIds: async () => [],
52
+ fetchByIds: async () => [],
53
+ },
54
+ },
55
+ };
56
+
57
+ function wrapper({ children }: { children: ReactNode }) {
58
+ return <ConnectorProvider connector={stubConnector}>{children}</ConnectorProvider>;
59
+ }
60
+
61
+ describe('ConnectorProvider + useConnector', () => {
62
+ it('provides the connector through context', () => {
63
+ const { result } = renderHook(() => useConnector(), { wrapper });
64
+
65
+ expect(result.current).toBe(stubConnector);
66
+ expect(result.current.id).toBe('test');
67
+ expect(result.current.name).toBe('Test Connector');
68
+ });
69
+
70
+ it('throws when useConnector is called outside a provider', () => {
71
+ expect(() => {
72
+ renderHook(() => useConnector());
73
+ }).toThrow('useConnector() must be used within a <ConnectorProvider>');
74
+ });
75
+ });
76
+
77
+ describe('useProductTraits', () => {
78
+ it('returns the product traits from the active connector', () => {
79
+ const { result } = renderHook(() => useProductTraits(), { wrapper });
80
+
81
+ expect(result.current).toBe(stubConnector.traits.product);
82
+ expect(typeof result.current.getName).toBe('function');
83
+ expect(typeof result.current.getPrice).toBe('function');
84
+ });
85
+
86
+ it('traits work against a mock document', () => {
87
+ const { result } = renderHook(() => useProductTraits(), { wrapper });
88
+ const traits = result.current;
89
+
90
+ const doc = { id: '1', name: 'Widget' };
91
+ expect(traits.getName(doc)).toBe('Widget');
92
+ expect(traits.getId(doc)).toBe('1');
93
+ });
94
+
95
+ it('throws when called outside a provider', () => {
96
+ expect(() => {
97
+ renderHook(() => useProductTraits());
98
+ }).toThrow('useConnector() must be used within a <ConnectorProvider>');
99
+ });
100
+ });
@@ -0,0 +1,54 @@
1
+ import { createContext, useContext, type ReactNode } from 'react';
2
+
3
+ import type { TallyConnector } from '../types';
4
+
5
+ /**
6
+ * React context that holds the active connector.
7
+ * Wrap your app (or a section of it) in <ConnectorProvider> to make
8
+ * the connector's traits available to all Tally UI components below.
9
+ */
10
+ const ConnectorContext = createContext<TallyConnector | null>(null);
11
+
12
+ export interface ConnectorProviderProps {
13
+ connector: TallyConnector;
14
+ children: ReactNode;
15
+ }
16
+
17
+ export function ConnectorProvider({ connector, children }: ConnectorProviderProps) {
18
+ return (
19
+ <ConnectorContext.Provider value={connector}>
20
+ {children}
21
+ </ConnectorContext.Provider>
22
+ );
23
+ }
24
+
25
+ /**
26
+ * Access the active connector. Throws if used outside a ConnectorProvider.
27
+ */
28
+ export function useConnector(): TallyConnector {
29
+ const connector = useContext(ConnectorContext);
30
+ if (!connector) {
31
+ throw new Error(
32
+ 'useConnector() must be used within a <ConnectorProvider>. ' +
33
+ 'Wrap your app with <ConnectorProvider connector={yourConnector}>.'
34
+ );
35
+ }
36
+ return connector;
37
+ }
38
+
39
+ /**
40
+ * Convenience hook: grab just the product traits from the active connector.
41
+ */
42
+ export function useProductTraits() {
43
+ const connector = useConnector();
44
+ return connector.traits.product;
45
+ }
46
+
47
+ /**
48
+ * Convenience hook: grab just the customer traits from the active connector.
49
+ * Returns undefined if the connector doesn't implement customer traits.
50
+ */
51
+ export function useCustomerTraits() {
52
+ const connector = useConnector();
53
+ return connector.traits.customer;
54
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ // Types
2
+ export type {
3
+ TallyConnector,
4
+ ConnectorAuth,
5
+ ConnectorSchemas,
6
+ ConnectorTraits,
7
+ AuthField,
8
+ CollectionSync,
9
+ RemoteIdEntry,
10
+ ReplicationAdapter,
11
+ SyncContext,
12
+ ProductTraits,
13
+ CustomerTraits,
14
+ } from './types';
15
+
16
+ // Context & hooks
17
+ export {
18
+ ConnectorProvider,
19
+ useConnector,
20
+ useProductTraits,
21
+ useCustomerTraits,
22
+ } from './context/connector-context';
23
+ export type { ConnectorProviderProps } from './context/connector-context';
@@ -0,0 +1,125 @@
1
+ import type { RxJsonSchema } from 'rxdb';
2
+
3
+ import type { ProductTraits } from './traits/product';
4
+ import type { CustomerTraits } from './traits/customer';
5
+ import type { ReplicationAdapter } from './replication';
6
+
7
+ /**
8
+ * Authentication configuration for a connector.
9
+ * Each API has wildly different auth — JWT, API keys, OAuth, etc.
10
+ * The connector declares what it needs and how to use it.
11
+ */
12
+ export interface ConnectorAuth {
13
+ /** Human-readable auth type for UI display */
14
+ type: string;
15
+ /** Fields required from the user to authenticate */
16
+ fields: AuthField[];
17
+ /** Build request headers from stored credentials */
18
+ getHeaders: (credentials: Record<string, string>) => Record<string, string>;
19
+ /** Optional: validate that credentials work (e.g., test API call) */
20
+ validate?: (credentials: Record<string, string>) => Promise<boolean>;
21
+ }
22
+
23
+ export interface AuthField {
24
+ key: string;
25
+ label: string;
26
+ type: 'text' | 'password' | 'url';
27
+ placeholder?: string;
28
+ required?: boolean;
29
+ }
30
+
31
+ /**
32
+ * Sync configuration for a collection.
33
+ * Defines how to fetch data from the remote API and push changes back.
34
+ *
35
+ * @deprecated Use ReplicationAdapter instead. This interface will be removed
36
+ * once all connectors have migrated to the RxDB replication protocol.
37
+ */
38
+ export interface CollectionSync<T = any> {
39
+ /** Fetch all remote IDs (for diffing against local) */
40
+ fetchAllIds: (context: SyncContext) => Promise<RemoteIdEntry[]>;
41
+ /** Fetch documents by IDs */
42
+ fetchByIds: (ids: string[], context: SyncContext) => Promise<T[]>;
43
+ /** Fetch documents modified after a given date */
44
+ fetchModifiedAfter?: (date: string, context: SyncContext) => Promise<T[]>;
45
+ /** Push a local change to the remote */
46
+ push?: (doc: T, context: SyncContext) => Promise<T>;
47
+ /** Create a new document on the remote */
48
+ create?: (doc: Partial<T>, context: SyncContext) => Promise<T>;
49
+ /** Delete a document on the remote */
50
+ delete?: (id: string, context: SyncContext) => Promise<void>;
51
+ }
52
+
53
+ export interface RemoteIdEntry {
54
+ id: string;
55
+ dateModified?: string;
56
+ }
57
+
58
+ export interface SyncContext {
59
+ /** Unique connector identifier (used as replication namespace) */
60
+ connectorId: string;
61
+ /** Base URL for the API */
62
+ baseUrl: string;
63
+ /** Authenticated headers */
64
+ headers: Record<string, string>;
65
+ /** Optional abort signal */
66
+ signal?: AbortSignal;
67
+ }
68
+
69
+ /**
70
+ * Schema definitions for a connector.
71
+ * Each connector provides its own RxDB schemas that mirror its API shape.
72
+ */
73
+ export interface ConnectorSchemas {
74
+ products: RxJsonSchema<any>;
75
+ // Future: orders, customers, taxes, etc.
76
+ [key: string]: RxJsonSchema<any>;
77
+ }
78
+
79
+ /**
80
+ * Trait implementations for a connector.
81
+ * These are the accessor functions that components program against.
82
+ */
83
+ export interface ConnectorTraits {
84
+ product: ProductTraits;
85
+ customer?: CustomerTraits;
86
+ }
87
+
88
+ /**
89
+ * The main connector interface.
90
+ * Each backend (WooCommerce, Medusa, Vendure, etc.) implements this.
91
+ */
92
+ export interface TallyConnector {
93
+ /** Unique identifier for this connector */
94
+ id: string;
95
+ /** Human-readable name */
96
+ name: string;
97
+ /** Description of the backend this connects to */
98
+ description: string;
99
+ /** Icon or logo URL */
100
+ icon?: string;
101
+
102
+ /** Authentication configuration */
103
+ auth: ConnectorAuth;
104
+
105
+ /** RxDB schemas for each collection */
106
+ schemas: ConnectorSchemas;
107
+
108
+ /** Trait implementations — how to extract standard data from connector-specific docs */
109
+ traits: ConnectorTraits;
110
+
111
+ /**
112
+ * @deprecated Use `replication` instead.
113
+ * Sync configuration for each collection (legacy pull-based sync).
114
+ */
115
+ sync: {
116
+ products: CollectionSync;
117
+ [key: string]: CollectionSync;
118
+ };
119
+
120
+ /** Replication adapters for each collection (RxDB replication protocol) */
121
+ replication?: {
122
+ products?: ReplicationAdapter<any>;
123
+ [key: string]: ReplicationAdapter<any> | undefined;
124
+ };
125
+ }
@@ -0,0 +1,16 @@
1
+ export type {
2
+ TallyConnector,
3
+ ConnectorAuth,
4
+ ConnectorSchemas,
5
+ ConnectorTraits,
6
+ AuthField,
7
+ CollectionSync,
8
+ RemoteIdEntry,
9
+ SyncContext,
10
+ } from './connector';
11
+
12
+ export type { ReplicationAdapter } from './replication';
13
+
14
+ export type { ProductTraits } from './traits/product';
15
+
16
+ export type { CustomerTraits } from './traits/customer';
@@ -0,0 +1,20 @@
1
+ import { describe, expectTypeOf, it } from 'vitest';
2
+ import type { ReplicationAdapter, SyncContext } from '@tallyui/core';
3
+
4
+ describe('ReplicationAdapter types', () => {
5
+ it('accepts a typed adapter with custom checkpoint', () => {
6
+ type Product = { id: string; name: string; _deleted: boolean };
7
+ type Checkpoint = { id: string; modified: string };
8
+
9
+ const adapter: ReplicationAdapter<Product, Checkpoint> = {} as any;
10
+
11
+ expectTypeOf(adapter.pull.handler).toBeFunction();
12
+ expectTypeOf(adapter.push).toEqualTypeOf<ReplicationAdapter<Product, Checkpoint>['push']>();
13
+ });
14
+
15
+ it('requires _deleted on document type', () => {
16
+ type Doc = { id: string; _deleted: boolean };
17
+ type Adapter = ReplicationAdapter<Doc>;
18
+ expectTypeOf<Adapter['pull']['handler']>().toBeFunction();
19
+ });
20
+ });
@@ -0,0 +1,51 @@
1
+ import type { RxReplicationPullStreamItem, RxReplicationWriteToMasterRow } from 'rxdb';
2
+ import type { Observable } from 'rxjs';
3
+
4
+ import type { SyncContext } from './connector';
5
+
6
+ /**
7
+ * Replication adapter interface for a single collection.
8
+ *
9
+ * Each connector implements this to provide pull/push handlers
10
+ * compatible with RxDB's replicateRxCollection protocol.
11
+ *
12
+ * CheckpointType is connector-specific (cursor, offset+timestamp, etc).
13
+ */
14
+ export interface ReplicationAdapter<RxDocType, CheckpointType = any> {
15
+ pull: {
16
+ /**
17
+ * Fetch documents changed since the last checkpoint.
18
+ * Return an empty documents array when fully caught up.
19
+ */
20
+ handler: (
21
+ lastCheckpoint: CheckpointType | undefined,
22
+ batchSize: number,
23
+ context: SyncContext
24
+ ) => Promise<{
25
+ documents: Array<RxDocType & { _deleted: boolean }>;
26
+ checkpoint: CheckpointType;
27
+ }>;
28
+
29
+ /**
30
+ * Optional real-time event stream.
31
+ * Emit { documents, checkpoint } for live updates,
32
+ * or 'RESYNC' to trigger a full pull cycle.
33
+ */
34
+ stream$?: Observable<RxReplicationPullStreamItem<RxDocType, CheckpointType>>;
35
+ };
36
+
37
+ push?: {
38
+ /**
39
+ * Push local changes to the remote API.
40
+ * Return an array of conflict documents (server's version wins).
41
+ * Return empty array if no conflicts.
42
+ */
43
+ handler: (
44
+ changeRows: RxReplicationWriteToMasterRow<RxDocType>[],
45
+ context: SyncContext
46
+ ) => Promise<Array<RxDocType & { _deleted: boolean }>>;
47
+
48
+ /** Max documents per push batch. Defaults to 100. */
49
+ batchSize?: number;
50
+ };
51
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Customer traits — the stable interface that UI components program against.
3
+ *
4
+ * Each connector implements these to extract standard customer data
5
+ * from its own schema shape.
6
+ */
7
+ export interface CustomerTraits<Doc = any> {
8
+ /** Customer display name (first + last or company) */
9
+ getName: (doc: Doc) => string;
10
+
11
+ /** Email address */
12
+ getEmail: (doc: Doc) => string | undefined;
13
+
14
+ /** Phone number */
15
+ getPhone: (doc: Doc) => string | undefined;
16
+
17
+ /** One-line address summary (e.g., "123 Main St, Springfield") */
18
+ getAddressSummary: (doc: Doc) => string | undefined;
19
+
20
+ /** The connector-specific unique ID (as string for consistency) */
21
+ getId: (doc: Doc) => string;
22
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Product traits — the stable interface that UI components program against.
3
+ *
4
+ * Each connector implements these accessors to extract standard product data
5
+ * from its own schema shape. Components call these instead of reaching into
6
+ * the raw document, so they work identically across WooCommerce, Medusa,
7
+ * Vendure, Shopify, etc.
8
+ *
9
+ * The generic `Doc` type is the connector's raw RxDB document type.
10
+ */
11
+ export interface ProductTraits<Doc = any> {
12
+ /** Product display name */
13
+ getName: (doc: Doc) => string;
14
+
15
+ /** SKU / stock keeping unit */
16
+ getSku: (doc: Doc) => string | undefined;
17
+
18
+ /** Current selling price as a string (connector-native format) */
19
+ getPrice: (doc: Doc) => string | undefined;
20
+
21
+ /** Regular (non-sale) price */
22
+ getRegularPrice: (doc: Doc) => string | undefined;
23
+
24
+ /** Sale price, if on sale */
25
+ getSalePrice: (doc: Doc) => string | undefined;
26
+
27
+ /** Whether the product is currently on sale */
28
+ isOnSale: (doc: Doc) => boolean;
29
+
30
+ /** Primary image URL */
31
+ getImageUrl: (doc: Doc) => string | undefined;
32
+
33
+ /** All image URLs */
34
+ getImageUrls: (doc: Doc) => string[];
35
+
36
+ /** Short description / excerpt */
37
+ getDescription: (doc: Doc) => string | undefined;
38
+
39
+ /** Stock status */
40
+ getStockStatus: (doc: Doc) => 'instock' | 'outofstock' | 'onbackorder' | 'unknown';
41
+
42
+ /** Stock quantity (null if not tracked) */
43
+ getStockQuantity: (doc: Doc) => number | null;
44
+
45
+ /** Whether this product has variants/variations */
46
+ hasVariants: (doc: Doc) => boolean;
47
+
48
+ /** Product type (simple, variable, etc. — connector-specific but useful for UI hints) */
49
+ getType: (doc: Doc) => string;
50
+
51
+ /** Barcode / UPC / EAN */
52
+ getBarcode: (doc: Doc) => string | undefined;
53
+
54
+ /** Categories as simple label strings */
55
+ getCategoryNames: (doc: Doc) => string[];
56
+
57
+ /** The connector-specific unique ID (as string for consistency) */
58
+ getId: (doc: Doc) => string;
59
+ }