ofsync-bridge-ofpos 0.1.0-alpha.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 +37 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.esm.js +1 -0
- package/dist/index.js +146 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# ofsync-bridge-ofpos
|
|
2
|
+
|
|
3
|
+
Bridge sinkronisasi domain **ofpos** untuk runtime `ofsync-shared-core`.
|
|
4
|
+
|
|
5
|
+
Package ini menyediakan registrar bridge domain agar host app dapat menghubungkan perubahan lokal/remote domain `ofpos` ke pipeline sinkronisasi host-side secara konsisten.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install ofsync-bridge-ofpos
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Usage
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { registerSyncBridge } from 'ofsync-bridge-ofpos';
|
|
17
|
+
|
|
18
|
+
registerSyncBridge(runtime, {
|
|
19
|
+
async collectSyncChanges(scope) {
|
|
20
|
+
return [];
|
|
21
|
+
},
|
|
22
|
+
async applySyncChanges(changes, scope) {
|
|
23
|
+
// map ke use-case domain ofpos
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Development
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install --no-audit --no-fund
|
|
32
|
+
npm run ci:check
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Documentation
|
|
36
|
+
|
|
37
|
+
- Internal docs hub: `docs/00-DOCUMENTATION-HUB.md`
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { DbAdapter } from 'ofpos-shared-core';
|
|
2
|
+
export interface SyncScope {
|
|
3
|
+
tenantId?: string;
|
|
4
|
+
branchId?: string;
|
|
5
|
+
deviceId?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SyncChange {
|
|
8
|
+
id: string;
|
|
9
|
+
domain: string;
|
|
10
|
+
entity: string;
|
|
11
|
+
type: 'CREATE' | 'UPDATE' | 'DELETE';
|
|
12
|
+
recordId: string;
|
|
13
|
+
payload: Record<string, unknown>;
|
|
14
|
+
occurredAt: string;
|
|
15
|
+
scope?: SyncScope;
|
|
16
|
+
}
|
|
17
|
+
export interface DomainBridge {
|
|
18
|
+
domain: string;
|
|
19
|
+
collectLocalChanges?: (scope: SyncScope) => Promise<SyncChange[]>;
|
|
20
|
+
applyRemoteChanges?: (changes: SyncChange[], scope: SyncScope) => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export interface SyncRuntimeLike {
|
|
23
|
+
registerDomainBridge: (bridge: DomainBridge) => void;
|
|
24
|
+
}
|
|
25
|
+
export declare const BRIDGE_DOMAIN = "ofpos";
|
|
26
|
+
export interface DomainSyncBridgePort {
|
|
27
|
+
collectSyncChanges?: (scope: SyncScope) => Promise<SyncChange[]>;
|
|
28
|
+
applySyncChanges?: (changes: SyncChange[], scope: SyncScope) => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Default table name mapper for POS domain.
|
|
32
|
+
* Prefixes tables in POS_DOMAIN_TABLES set with 'ofpos_'.
|
|
33
|
+
*/
|
|
34
|
+
export declare const createPosTableNameMapper: (domainTables: string[] | Set<string> | readonly string[]) => (tableName: string) => string;
|
|
35
|
+
export interface PosSyncBridgeOptions {
|
|
36
|
+
dbAdapter: DbAdapter;
|
|
37
|
+
onChangesApplied?: (changes: SyncChange[]) => void | Promise<void>;
|
|
38
|
+
defaultBranchId?: string;
|
|
39
|
+
inventoryCacheInvalidator?: {
|
|
40
|
+
invalidateProductsStock?: (productIds: string[]) => void | Promise<void>;
|
|
41
|
+
clearStockCache?: () => void | Promise<void>;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Creates a standard POS sync bridge.
|
|
46
|
+
* Encapsulates the logic for applying remote changes into the local database
|
|
47
|
+
* with proper table prefixing and scoping.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createPosSyncBridge(options: PosSyncBridgeOptions): DomainBridge;
|
|
50
|
+
export declare function registerSyncBridge(runtime: SyncRuntimeLike, portOrBridge: DomainSyncBridgePort | DomainBridge): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{POS_DOMAIN_TABLES as C}from"ofpos-shared-core";const p="ofpos";function I(t){if(!t||typeof t!="object")return null;const n=t;return typeof n.applyRemoteChanges=="function"||typeof n.collectLocalChanges=="function"||typeof n.domain=="string"&&n.domain.trim().length>0?n:null}const B=t=>{const n=t instanceof Set?t:new Set(t);return e=>e.startsWith("ofpos_")||e.startsWith("ofauth_")||e.startsWith("ofsync_")||e.startsWith("__")?e:n.has(e)?`ofpos_${e}`:e};function D(t){const n=new Set;for(const e of t){const o=e.payload,r=String(o.table??e.entity??"");if(r!=="inventory_logs"&&r!=="ofpos_inventory_logs")continue;const g=o.record&&typeof o.record=="object"?o.record:{},c=String(g.productId??"");c&&n.add(c)}return[...n]}function w(t){const{dbAdapter:n,onChangesApplied:e,defaultBranchId:o,inventoryCacheInvalidator:r}=t,g=B(C);return{domain:p,collectLocalChanges:async()=>[],applyRemoteChanges:async(c,l)=>{const y=l.branchId||o||"",f=l.tenantId;for(const i of c){const d=i.payload,S=String(d.table??i.entity??""),h=d.record&&typeof d.record=="object"?d.record:{},a=String(i.recordId??h.id??"");if(!S||!a)continue;const s=g(S);if(i.type==="DELETE"){try{await n.delete(s,a)}catch{}continue}const u={...h,id:a,branchId:y,...f?{tenantId:f}:{}},m=await n.query(s,{filters:{and:[{field:"id",value:a},{field:"branchId",value:y}]},limit:1});(m.length>0?m:await n.query(s,{filters:{id:a},limit:1})).length>0?await n.update(s,a,u):await n.create(s,u)}if(r){const i=D(c);i.length>0&&typeof r.invalidateProductsStock=="function"?await r.invalidateProductsStock(i):typeof r.clearStockCache=="function"&&await r.clearStockCache()}e&&await e(c)}}}function R(t,n){const e=I(n);if(e){t.registerDomainBridge({domain:e.domain||p,collectLocalChanges:e.collectLocalChanges,applyRemoteChanges:e.applyRemoteChanges});return}const o=n;t.registerDomainBridge({domain:p,collectLocalChanges:o.collectSyncChanges,applyRemoteChanges:o.applySyncChanges})}export{p as BRIDGE_DOMAIN,w as createPosSyncBridge,B as createPosTableNameMapper,R as registerSyncBridge};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPosTableNameMapper = exports.BRIDGE_DOMAIN = void 0;
|
|
4
|
+
exports.createPosSyncBridge = createPosSyncBridge;
|
|
5
|
+
exports.registerSyncBridge = registerSyncBridge;
|
|
6
|
+
const ofpos_shared_core_1 = require("ofpos-shared-core");
|
|
7
|
+
exports.BRIDGE_DOMAIN = 'ofpos';
|
|
8
|
+
function asDomainBridgeCandidate(value) {
|
|
9
|
+
if (!value || typeof value !== 'object')
|
|
10
|
+
return null;
|
|
11
|
+
const candidate = value;
|
|
12
|
+
if (typeof candidate.applyRemoteChanges === 'function')
|
|
13
|
+
return candidate;
|
|
14
|
+
if (typeof candidate.collectLocalChanges === 'function')
|
|
15
|
+
return candidate;
|
|
16
|
+
if (typeof candidate.domain === 'string' && candidate.domain.trim().length > 0)
|
|
17
|
+
return candidate;
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Default table name mapper for POS domain.
|
|
22
|
+
* Prefixes tables in POS_DOMAIN_TABLES set with 'ofpos_'.
|
|
23
|
+
*/
|
|
24
|
+
const createPosTableNameMapper = (domainTables) => {
|
|
25
|
+
const tableSet = domainTables instanceof Set ? domainTables : new Set(domainTables);
|
|
26
|
+
return (tableName) => {
|
|
27
|
+
if (tableName.startsWith('ofpos_') ||
|
|
28
|
+
tableName.startsWith('ofauth_') ||
|
|
29
|
+
tableName.startsWith('ofsync_') ||
|
|
30
|
+
tableName.startsWith('__')) {
|
|
31
|
+
return tableName;
|
|
32
|
+
}
|
|
33
|
+
if (tableSet.has(tableName)) {
|
|
34
|
+
return `ofpos_${tableName}`;
|
|
35
|
+
}
|
|
36
|
+
return tableName;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
exports.createPosTableNameMapper = createPosTableNameMapper;
|
|
40
|
+
function collectInventoryLogProductIds(changes) {
|
|
41
|
+
const productIds = new Set();
|
|
42
|
+
for (const change of changes) {
|
|
43
|
+
const payload = change.payload;
|
|
44
|
+
const rawTable = String(payload.table ?? change.entity ?? '');
|
|
45
|
+
if (rawTable !== 'inventory_logs' && rawTable !== 'ofpos_inventory_logs')
|
|
46
|
+
continue;
|
|
47
|
+
const recordObj = payload.record && typeof payload.record === 'object'
|
|
48
|
+
? payload.record
|
|
49
|
+
: {};
|
|
50
|
+
const productId = String(recordObj.productId ?? '');
|
|
51
|
+
if (productId)
|
|
52
|
+
productIds.add(productId);
|
|
53
|
+
}
|
|
54
|
+
return [...productIds];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Creates a standard POS sync bridge.
|
|
58
|
+
* Encapsulates the logic for applying remote changes into the local database
|
|
59
|
+
* with proper table prefixing and scoping.
|
|
60
|
+
*/
|
|
61
|
+
function createPosSyncBridge(options) {
|
|
62
|
+
const { dbAdapter, onChangesApplied, defaultBranchId, inventoryCacheInvalidator } = options;
|
|
63
|
+
const mapTable = (0, exports.createPosTableNameMapper)(ofpos_shared_core_1.POS_DOMAIN_TABLES);
|
|
64
|
+
return {
|
|
65
|
+
domain: exports.BRIDGE_DOMAIN,
|
|
66
|
+
collectLocalChanges: async () => [], // Host-managed collect is default
|
|
67
|
+
applyRemoteChanges: async (changes, scope) => {
|
|
68
|
+
const branchId = scope.branchId || defaultBranchId || '';
|
|
69
|
+
const tenantId = scope.tenantId;
|
|
70
|
+
for (const change of changes) {
|
|
71
|
+
const payload = change.payload;
|
|
72
|
+
const rawTable = String(payload.table ?? change.entity ?? '');
|
|
73
|
+
const recordObj = payload.record && typeof payload.record === 'object'
|
|
74
|
+
? payload.record
|
|
75
|
+
: {};
|
|
76
|
+
const recordId = String(change.recordId ?? recordObj.id ?? '');
|
|
77
|
+
if (!rawTable || !recordId)
|
|
78
|
+
continue;
|
|
79
|
+
const tableName = mapTable(rawTable);
|
|
80
|
+
if (change.type === 'DELETE') {
|
|
81
|
+
try {
|
|
82
|
+
await dbAdapter.delete(tableName, recordId);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Delete idempotent
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const upsertPayload = {
|
|
90
|
+
...recordObj,
|
|
91
|
+
id: recordId,
|
|
92
|
+
branchId,
|
|
93
|
+
...(tenantId ? { tenantId } : {}),
|
|
94
|
+
};
|
|
95
|
+
// Prefer scoped lookup first.
|
|
96
|
+
const existingScoped = await dbAdapter.query(tableName, {
|
|
97
|
+
filters: { and: [{ field: 'id', value: recordId }, { field: 'branchId', value: branchId }] },
|
|
98
|
+
limit: 1,
|
|
99
|
+
});
|
|
100
|
+
// Fallback for legacy local rows that may exist with stale/missing scope.
|
|
101
|
+
const existingAnyScope = existingScoped.length > 0
|
|
102
|
+
? existingScoped
|
|
103
|
+
: await dbAdapter.query(tableName, {
|
|
104
|
+
filters: { id: recordId },
|
|
105
|
+
limit: 1,
|
|
106
|
+
});
|
|
107
|
+
if (existingAnyScope.length > 0) {
|
|
108
|
+
await dbAdapter.update(tableName, recordId, upsertPayload);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
await dbAdapter.create(tableName, upsertPayload);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (inventoryCacheInvalidator) {
|
|
115
|
+
const invalidatedProductIds = collectInventoryLogProductIds(changes);
|
|
116
|
+
if (invalidatedProductIds.length > 0 &&
|
|
117
|
+
typeof inventoryCacheInvalidator.invalidateProductsStock === 'function') {
|
|
118
|
+
await inventoryCacheInvalidator.invalidateProductsStock(invalidatedProductIds);
|
|
119
|
+
}
|
|
120
|
+
else if (typeof inventoryCacheInvalidator.clearStockCache === 'function') {
|
|
121
|
+
await inventoryCacheInvalidator.clearStockCache();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (onChangesApplied) {
|
|
125
|
+
await onChangesApplied(changes);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function registerSyncBridge(runtime, portOrBridge) {
|
|
131
|
+
const directBridge = asDomainBridgeCandidate(portOrBridge);
|
|
132
|
+
if (directBridge) {
|
|
133
|
+
runtime.registerDomainBridge({
|
|
134
|
+
domain: directBridge.domain || exports.BRIDGE_DOMAIN,
|
|
135
|
+
collectLocalChanges: directBridge.collectLocalChanges,
|
|
136
|
+
applyRemoteChanges: directBridge.applyRemoteChanges,
|
|
137
|
+
});
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const port = portOrBridge;
|
|
141
|
+
runtime.registerDomainBridge({
|
|
142
|
+
domain: exports.BRIDGE_DOMAIN,
|
|
143
|
+
collectLocalChanges: port.collectSyncChanges,
|
|
144
|
+
applyRemoteChanges: port.applySyncChanges,
|
|
145
|
+
});
|
|
146
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ofsync-bridge-ofpos",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Sync bridge adapter for wiring ofpos domain changes into ofsync-shared-core runtime.",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Agus Made",
|
|
8
|
+
"email": "krisnaparta@gmail.com"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"module": "dist/index.esm.js",
|
|
12
|
+
"types": "dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.esm.js",
|
|
17
|
+
"require": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
25
|
+
"build": "npm run clean && tsc -p tsconfig.build.json && node esbuild.config.js",
|
|
26
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
27
|
+
"test": "npm run build && node --test ./tests/*.test.js",
|
|
28
|
+
"verify:surface": "node ./scripts/verify-surface.js",
|
|
29
|
+
"verify:boundary": "node ./scripts/verify-boundary-imports.js",
|
|
30
|
+
"ci:check": "npm run typecheck && npm run verify:surface && npm run verify:boundary && npm run test",
|
|
31
|
+
"prepublishOnly": "npm run ci:check",
|
|
32
|
+
"prepack": "npm run build"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"ofpos-shared-core": "2.0.0-alpha.0",
|
|
36
|
+
"esbuild": "^0.25.11",
|
|
37
|
+
"typescript": "^5.9.3",
|
|
38
|
+
"@types/node": "^20.19.0"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"ofsync-shared-core": "0.2.0-alpha.1",
|
|
45
|
+
"ofpos-shared-core": "2.0.0-alpha.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"ofsync-shared-core": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"ofpos-shared-core": {
|
|
52
|
+
"optional": false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|