blixify-server 1.0.0 → 1.0.2

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 CHANGED
@@ -1 +1,160 @@
1
1
  # Blixify Server
2
+
3
+ Shared server template: MongoDB, Firebase, Auth, Upload, and real-time Pub/Sub→SSE live-stream.
4
+
5
+ ---
6
+
7
+ ## SocketWrapper
8
+
9
+ Generalises Verdant's `emitDeliveryEvent` / `getPubSubClient` pattern for any collection.
10
+
11
+ ```
12
+ Write → MongoWrapper.afterWrite → SocketWrapper.emit
13
+
14
+ Pub/Sub topic (global bus)
15
+ attributes.room
16
+
17
+ ┌─────────────────────┴─────────────────────┐
18
+ room="robots" room="robots-r001"
19
+ (list-level SSE clients) (doc-level SSE clients)
20
+ ```
21
+
22
+ Every GAE instance publishes to the same Pub/Sub topic. Each SSE client gets its own temporary pull subscription filtered by `attributes.room`. A client on instance 2 receives events published by instance 1 — no Redis adapter, no sticky sessions.
23
+
24
+ ### Room naming
25
+
26
+ | Room | Who subscribes | What it receives |
27
+ | --------------- | --------------------------------------------------------- | ------------------------------------ |
28
+ | `"robots"` | DTW **list** view (`bareSocketName="robots"`) | insert / update / delete for any doc |
29
+ | `"robots-r001"` | DTW **read/update** view (`bareSocketName="robots-r001"`) | update / delete for doc r001 only |
30
+
31
+ On every write, `emit("robots", "r001", "update", payload)` publishes to **both** rooms.
32
+
33
+ ### Install
34
+
35
+ ```bash
36
+ yarn add @google-cloud/pubsub
37
+ ```
38
+
39
+ ### GCP setup
40
+
41
+ 1. Create a Pub/Sub topic — e.g. `blixify-live-events-prod` (separate from your Verdant topics).
42
+ 2. Grant `pubsub.topics.publish` to the App Engine service account.
43
+ 3. Set `PUBSUB_LIVE_TOPIC=blixify-live-events-prod` in `.env` / App Engine env vars.
44
+
45
+ ### Server setup
46
+
47
+ ```ts
48
+ import { PubSub } from "@google-cloud/pubsub";
49
+ import { SocketWrapper } from "blixify-server/dist/apis";
50
+
51
+ // INFO: mirrors Verdant's getPubSubClient — null → no-op in local dev
52
+ const pubsub = process.env.PUBSUB_LIVE_TOPIC ? new PubSub() : null;
53
+
54
+ export const socketWrapper = new SocketWrapper(
55
+ pubsub,
56
+ process.env.PUBSUB_LIVE_TOPIC ?? "",
57
+ ["robots", "vehicles"], // INFO: opt-in whitelist
58
+ {
59
+ checkTopicAuth: async (room, req) =>
60
+ req.query?.bm_apiToken === process.env.SECURITY_TOKEN ||
61
+ req.body?.bm_apiToken === process.env.SECURITY_TOKEN,
62
+ },
63
+ );
64
+ ```
65
+
66
+ ### Wire to MongoWrapper
67
+
68
+ ```ts
69
+ // afterWrite is optional — when unset, MongoWrapper behaviour is unchanged
70
+ wrapper.afterWrite = (type, id, payload) =>
71
+ socketWrapper.emit("robots", id, type, payload);
72
+ // ↑
73
+ // id="" for batch ops → only collection room receives it
74
+ ```
75
+
76
+ ### SSE endpoint
77
+
78
+ **Next.js:**
79
+
80
+ ```ts
81
+ // pages/api/live/[room].ts
82
+ export const config = { api: { bodyParser: false } };
83
+ export default socketWrapper.createSSEHandler();
84
+ ```
85
+
86
+ **Express:**
87
+
88
+ ```ts
89
+ app.get("/api/live/:room", socketWrapper.createSSEHandler());
90
+ ```
91
+
92
+ ### Snapshot on reconnect
93
+
94
+ ```ts
95
+ socketWrapper.registerSnapshotProvider("robots", async () => {
96
+ const db = await mongoPromise;
97
+ return db.db("robots-prod").collection("robots").find({}).toArray();
98
+ });
99
+ ```
100
+
101
+ On each new SSE connection the handler immediately sends a `snapshot` envelope so the client reconciles rows that drifted while offline.
102
+
103
+ ### SocketEnvelope
104
+
105
+ ```ts
106
+ interface SocketEnvelope<T = any> {
107
+ topic: string; // collection name
108
+ type: "insert" | "update" | "delete" | "snapshot";
109
+ payload: T | T[]; // snapshot = T[]
110
+ ts: number; // Unix ms
111
+ }
112
+ ```
113
+
114
+ ### DTW client usage
115
+
116
+ Set `workspace.sseEndpoint` (or `devSettings.sseEndpoint`) to the base of your `/api/live` route:
117
+
118
+ ```tsx
119
+ // workspaceDev
120
+ export const workspaceDev = {
121
+ apiEndpoint: `${apiPrefix}/api/data`,
122
+ assetEndpoint: storageAPI,
123
+ assetUploadEndpoint: `${apiPrefix}/api/asset`,
124
+ apiEndpointToken: securityToken,
125
+ sseEndpoint: `${apiPrefix}/api/live`, // ← add this
126
+ };
127
+
128
+ // List view — subscribes room "robots"
129
+ <DataTemplateWrapper collectionId="robots" type="list"
130
+ bareSettings={{ bareSocketName: "robots" }}
131
+ workspace={workspaceDev} />
132
+
133
+ // Read/update view — subscribes room "robots-{id}"
134
+ <DataTemplateWrapper collectionId="robots" type="read" id={robotId}
135
+ bareSettings={{ bareSocketName: `robots-${robotId}` }}
136
+ workspace={workspaceDev} />
137
+
138
+ // MRQ polling fallback (1.5 s) — no Pub/Sub needed
139
+ <DataTemplateWrapper collectionId="robots" type="list"
140
+ bareSettings={{ barePollingInterval: 1500 }}
141
+ workspace={workspaceDev} />
142
+ ```
143
+
144
+ The DTW opens `EventSource` at `{sseEndpoint}/{room}?bm_apiToken={token}`. No socket.io-client package needed.
145
+
146
+ ### Capacitor
147
+
148
+ EventSource is a plain HTTP long-lived GET. Works on iOS and Android WebView with no extra configuration — same as any other API call. No sticky sessions or `instance_affinity` required on GAE.
149
+
150
+ ### Local dev
151
+
152
+ `pubsub = null` → `emit()` is a silent no-op. The SSE endpoint stays open and sends periodic `: ping` keep-alives. The app saves correctly; other devices just don't receive a live push.
153
+
154
+ ### Tests
155
+
156
+ ```bash
157
+ yarn test:unit
158
+ ```
159
+
160
+ Covers: whitelist gating, dual-room publish (collection + doc), batch-op single-room publish, null-pubsub no-op, SSE auth rejection, snapshot on connect, `MongoWrapper.afterWrite` → `emit` full chain.
@@ -5,6 +5,10 @@ export { GoogleAnalyticsWrapper } from "./googleAnalyticsWrapper";
5
5
  export { MondayWrapper } from "./mondayWrapper";
6
6
  export { MongoWrapper } from "./mongoWrapper";
7
7
  export { SecurityMiddleware } from "./security";
8
+ export { SocketWrapper } from "./socketWrapper";
9
+ export type { SocketWrapperOptions } from "./socketWrapper";
8
10
  export { TrackVisionWrapper } from "./trackVisionWrapper";
9
11
  export { UploadWrapper } from "./uploadWrapper";
12
+ export type { SocketEnvelope } from "../model/socket";
13
+ export { buildListRoom, buildDocRoom } from "../model/socket";
10
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/apis/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/apis/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
@@ -1 +1 @@
1
- Object.defineProperty(exports,"__esModule",{value:!0}),exports.UploadWrapper=exports.TrackVisionWrapper=exports.SecurityMiddleware=exports.MongoWrapper=exports.MondayWrapper=exports.GoogleAnalyticsWrapper=exports.FirebaseWrapper=exports.CryptoMiddleware=exports.AuthWrapper=void 0;var authWrapper_1=require("./authWrapper"),crypto_1=(Object.defineProperty(exports,"AuthWrapper",{enumerable:!0,get:function(){return authWrapper_1.AuthWrapper}}),require("./crypto")),fbWrapper_1=(Object.defineProperty(exports,"CryptoMiddleware",{enumerable:!0,get:function(){return crypto_1.CryptoMiddleware}}),require("./fbWrapper")),googleAnalyticsWrapper_1=(Object.defineProperty(exports,"FirebaseWrapper",{enumerable:!0,get:function(){return fbWrapper_1.FirebaseWrapper}}),require("./googleAnalyticsWrapper")),mondayWrapper_1=(Object.defineProperty(exports,"GoogleAnalyticsWrapper",{enumerable:!0,get:function(){return googleAnalyticsWrapper_1.GoogleAnalyticsWrapper}}),require("./mondayWrapper")),mongoWrapper_1=(Object.defineProperty(exports,"MondayWrapper",{enumerable:!0,get:function(){return mondayWrapper_1.MondayWrapper}}),require("./mongoWrapper")),security_1=(Object.defineProperty(exports,"MongoWrapper",{enumerable:!0,get:function(){return mongoWrapper_1.MongoWrapper}}),require("./security")),trackVisionWrapper_1=(Object.defineProperty(exports,"SecurityMiddleware",{enumerable:!0,get:function(){return security_1.SecurityMiddleware}}),require("./trackVisionWrapper")),uploadWrapper_1=(Object.defineProperty(exports,"TrackVisionWrapper",{enumerable:!0,get:function(){return trackVisionWrapper_1.TrackVisionWrapper}}),require("./uploadWrapper"));Object.defineProperty(exports,"UploadWrapper",{enumerable:!0,get:function(){return uploadWrapper_1.UploadWrapper}});
1
+ Object.defineProperty(exports,"__esModule",{value:!0}),exports.buildDocRoom=exports.buildListRoom=exports.UploadWrapper=exports.TrackVisionWrapper=exports.SocketWrapper=exports.SecurityMiddleware=exports.MongoWrapper=exports.MondayWrapper=exports.GoogleAnalyticsWrapper=exports.FirebaseWrapper=exports.CryptoMiddleware=exports.AuthWrapper=void 0;var authWrapper_1=require("./authWrapper"),crypto_1=(Object.defineProperty(exports,"AuthWrapper",{enumerable:!0,get:function(){return authWrapper_1.AuthWrapper}}),require("./crypto")),fbWrapper_1=(Object.defineProperty(exports,"CryptoMiddleware",{enumerable:!0,get:function(){return crypto_1.CryptoMiddleware}}),require("./fbWrapper")),googleAnalyticsWrapper_1=(Object.defineProperty(exports,"FirebaseWrapper",{enumerable:!0,get:function(){return fbWrapper_1.FirebaseWrapper}}),require("./googleAnalyticsWrapper")),mondayWrapper_1=(Object.defineProperty(exports,"GoogleAnalyticsWrapper",{enumerable:!0,get:function(){return googleAnalyticsWrapper_1.GoogleAnalyticsWrapper}}),require("./mondayWrapper")),mongoWrapper_1=(Object.defineProperty(exports,"MondayWrapper",{enumerable:!0,get:function(){return mondayWrapper_1.MondayWrapper}}),require("./mongoWrapper")),security_1=(Object.defineProperty(exports,"MongoWrapper",{enumerable:!0,get:function(){return mongoWrapper_1.MongoWrapper}}),require("./security")),socketWrapper_1=(Object.defineProperty(exports,"SecurityMiddleware",{enumerable:!0,get:function(){return security_1.SecurityMiddleware}}),require("./socketWrapper")),trackVisionWrapper_1=(Object.defineProperty(exports,"SocketWrapper",{enumerable:!0,get:function(){return socketWrapper_1.SocketWrapper}}),require("./trackVisionWrapper")),uploadWrapper_1=(Object.defineProperty(exports,"TrackVisionWrapper",{enumerable:!0,get:function(){return trackVisionWrapper_1.TrackVisionWrapper}}),require("./uploadWrapper")),socket_1=(Object.defineProperty(exports,"UploadWrapper",{enumerable:!0,get:function(){return uploadWrapper_1.UploadWrapper}}),require("../model/socket"));Object.defineProperty(exports,"buildListRoom",{enumerable:!0,get:function(){return socket_1.buildListRoom}}),Object.defineProperty(exports,"buildDocRoom",{enumerable:!0,get:function(){return socket_1.buildDocRoom}});
@@ -1 +1 @@
1
- {"version":3,"file":"mondayWrapper.d.ts","sourceRoot":"","sources":["../../src/apis/mondayWrapper.ts"],"names":[],"mappings":"AAEA,OAAO,cAAc,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AA2B5C,eAAO,MAAM,cAAc,GACzB,OAAO,MAAM,EACb,OAAO,GAAG,EACV,YAAY,GAAG,qDAchB,CAAC;AAEF;;;;GAIG;AACH,qBAAa,aAAa;IACxB,WAAW,SAAM;IACjB,OAAO,SAAM;IACb,OAAO,SAAM;IACb,MAAM,EAAE,cAAc,CAQpB;IACF,GAAG,EAAE,UAAU,CAAC;IAEhB,YAAY,GAAI,KAAK,GAAG,aAEtB;IACF,SAAS,QAAO;QAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE;YAAE,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;SAAE,CAAA;KAAE,CAKjE;gBAGA,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,cAAc,EACtB,YAAY,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,EACrD,SAAS,EAAE,MAAM;QAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE;YAAE,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;SAAE,CAAA;KAAE,EACpE,GAAG,EAAE,UAAU;IAWjB,UAAU,GAAI,YAAY;QACxB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,EAAE;YACb,EAAE,EAAE,MAAM,CAAC;YACX,IAAI,EAAE,MAAM,CAAC;YACb,KAAK,EAAE,MAAM,CAAC;YACd,IAAI,CAAC,EAAE,MAAM,CAAC;YACd,aAAa,CAAC,EAAE,MAAM,CAAC;SACxB,EAAE,CAAC;KACL,SAiBC;IAEF,UAAU,GACR,KAAK,GAAG,EACR,KAAK,GAAG,EACR,WAAW,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;;mBAgEtC;IAEF,OAAO,GAAU,KAAK,GAAG,EAAE,KAAK,GAAG;;mBAwDjC;IAEF,QAAQ,GAAU,KAAK,GAAG,EAAE,KAAK,GAAG;;;mBAgQlC;IAEF,UAAU,GAAU,KAAK,GAAG,EAAE,KAAK,GAAG;;mBAsFpC;IAEF,UAAU,GACR,KAAK,GAAG,EACR,KAAK,GAAG,EACR,WAAW,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;;mBA8DtC;IAEF,IAAI,YA8CF;CACH"}
1
+ {"version":3,"file":"mondayWrapper.d.ts","sourceRoot":"","sources":["../../src/apis/mondayWrapper.ts"],"names":[],"mappings":"AAEA,OAAO,cAAc,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AA2B5C,eAAO,MAAM,cAAc,GACzB,OAAO,MAAM,EACb,OAAO,GAAG,EACV,YAAY,GAAG,qDAchB,CAAC;AAEF;;;;GAIG;AACH,qBAAa,aAAa;IACxB,WAAW,SAAM;IACjB,OAAO,SAAM;IACb,OAAO,SAAM;IACb,MAAM,EAAE,cAAc,CAQpB;IACF,GAAG,EAAE,UAAU,CAAC;IAEhB,YAAY,GAAI,KAAK,GAAG,aAEtB;IACF,SAAS,QAAO;QAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE;YAAE,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;SAAE,CAAA;KAAE,CAKjE;gBAGA,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,cAAc,EACtB,YAAY,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,EACrD,SAAS,EAAE,MAAM;QAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE;YAAE,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;SAAE,CAAA;KAAE,EACpE,GAAG,EAAE,UAAU;IAWjB,UAAU,GAAI,YAAY;QACxB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,EAAE;YACb,EAAE,EAAE,MAAM,CAAC;YACX,IAAI,EAAE,MAAM,CAAC;YACb,KAAK,EAAE,MAAM,CAAC;YACd,IAAI,CAAC,EAAE,MAAM,CAAC;YACd,aAAa,CAAC,EAAE,MAAM,CAAC;SACxB,EAAE,CAAC;KACL,SAiBC;IAEF,UAAU,GACR,KAAK,GAAG,EACR,KAAK,GAAG,EACR,WAAW,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;;mBAiEtC;IAEF,OAAO,GAAU,KAAK,GAAG,EAAE,KAAK,GAAG;;mBAyDjC;IAEF,QAAQ,GAAU,KAAK,GAAG,EAAE,KAAK,GAAG;;;mBAgQlC;IAEF,UAAU,GAAU,KAAK,GAAG,EAAE,KAAK,GAAG;;mBA6FpC;IAEF,UAAU,GACR,KAAK,GAAG,EACR,KAAK,GAAG,EACR,WAAW,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;;mBA+DtC;IAEF,IAAI,YA8CF;CACH"}
@@ -96,9 +96,9 @@ ${handleFormatRules(o)}
96
96
  }
97
97
  }
98
98
  }
99
- }`}p=yield(0,exports.queryMondayApi)(this.mondayToken,t),e=(i?p.data.data.next_items_page:p.data.data.boards[0].items_page).items;let o=!0,s=[];if(e.map(e=>{e=this.parseModel(e);s.push(e),(0,QueryModel_1.checkOpsConfig)(this.config,"read",h,e)||(o=!1)}),u&&o){let e=null;s.length===h.body.limit&&(e=(i?p.data.data.next_items_page:p.data.data.boards[0].items_page).cursor);var y={data:s,count:e};if(null!=(n=h.body)&&n.stopRes)return y;_.send(y)}else _.status(400).json({err:"Invalid Security Configuration"})}else _.status(400).json({err:"Invalid Fields"})}catch(e){_.status(400).json({err:e})}}),this.initUpdate=(d,n)=>__awaiter(this,void 0,void 0,function*(){var e;try{if(this.modelChecker(d.body.data)&&d.body.id){var t=`query {
99
+ }`}p=yield(0,exports.queryMondayApi)(this.mondayToken,t),e=(i?p.data.data.next_items_page:p.data.data.boards[0].items_page).items;let o=!0,s=[];if(e.map(e=>{e=this.parseModel(e);s.push(e),(0,QueryModel_1.checkOpsConfig)(this.config,"read",h,e)||(o=!1)}),u&&o){let e=null;s.length===h.body.limit&&(e=(i?p.data.data.next_items_page:p.data.data.boards[0].items_page).cursor);var y={data:s,count:e};if(null!=(n=h.body)&&n.stopRes)return y;_.send(y)}else _.status(400).json({err:"Invalid Security Configuration"})}else _.status(400).json({err:"Invalid Fields"})}catch(e){_.status(400).json({err:e})}}),this.initUpdate=(u,l)=>__awaiter(this,void 0,void 0,function*(){var e,t;try{if(this.modelChecker(u.body.data)&&u.body.id){var a=`query {
100
100
  boards(ids: ${this.boardId}) {
101
- items_page (limit: 1, query_params: {ids:${d.body.id}}) {
101
+ items_page (limit: 1, query_params: {ids:${u.body.id}}) {
102
102
  cursor
103
103
  items {
104
104
  id
@@ -111,17 +111,18 @@ ${handleFormatRules(o)}
111
111
  }
112
112
  }
113
113
  }
114
- }`,a=(yield(0,exports.queryMondayApi)(this.mondayToken,t)).data.data.boards[0].items_page.items;if(a.length<=0)throw"Error";var i=this.parseModel(a[0]),r=(0,QueryModel_1.checkBaseConfig)(this.config,d),o=(0,QueryModel_1.checkOpsConfig)(this.config,"update",d,i);if(r&&o){let i=d.body.data,r={boardId:this.boardId,itemId:d.body.id,columnValues:{}};Object.keys(i).map(t=>{if("id"!==t){var a=this.modelType().write;let e="";(e=a[t]?a[t]:e)?r.columnValues[t]={[e]:i[t]}:r.columnValues[t]=i[t]}}),r.columnValues=JSON.stringify(r.columnValues),yield(0,exports.queryMondayApi)(this.mondayToken,`
115
- mutation ($boardId: ID!, $itemId: ID!, $columnValues: JSON!) {
114
+ }`,i=(yield(0,exports.queryMondayApi)(this.mondayToken,a)).data.data.boards[0].items_page.items;if(i.length<=0)throw"Error";var r=this.parseModel(i[0]),o=(0,QueryModel_1.checkBaseConfig)(this.config,u),s=(0,QueryModel_1.checkOpsConfig)(this.config,"update",u,r);if(o&&s){var d=null!=(e=u.body.createLabelsIfMissing)&&e;let i=u.body.data,r={boardId:this.boardId,itemId:u.body.id,columnValues:{},createLabelsIfMissing:d};Object.keys(i).map(t=>{if("id"!==t){var a=this.modelType().write;let e="";(e=a[t]?a[t]:e)?r.columnValues[t]={[e]:i[t]}:r.columnValues[t]=i[t]}}),r.columnValues=JSON.stringify(r.columnValues),yield(0,exports.queryMondayApi)(this.mondayToken,`
115
+ mutation ($boardId: ID!, $itemId: ID!, $columnValues: JSON!, $createLabelsIfMissing: Boolean!) {
116
116
  change_multiple_column_values(
117
117
  item_id: $itemId,
118
118
  board_id: $boardId,
119
- column_values: $columnValues
119
+ column_values: $columnValues,
120
+ create_labels_if_missing: $createLabelsIfMissing
120
121
  ) {
121
122
  id
122
123
  }
123
124
  }
124
- `,r);var s={success:!0};if(null!=(e=d.body)&&e.stopRes)return s;n.send(s)}else n.status(400).json({err:"Invalid Security Configuration"})}else n.status(400).json({err:"Invalid Fields"})}catch(e){n.status(400).json({err:e})}}),this.initDelete=(n,u,l)=>__awaiter(this,void 0,void 0,function*(){var e;try{if(n.body.id){var t=`query {
125
+ `,r);var n={success:!0};if(null!=(t=u.body)&&t.stopRes)return n;l.send(n)}else l.status(400).json({err:"Invalid Security Configuration"})}else l.status(400).json({err:"Invalid Fields"})}catch(e){l.status(400).json({err:e})}}),this.initDelete=(n,u,l)=>__awaiter(this,void 0,void 0,function*(){var e;try{if(n.body.id){var t=`query {
125
126
  boards(ids: ${this.boardId}) {
126
127
  items_page (limit: 1, query_params: {ids:${n.body.id}}) {
127
128
  cursor
@@ -18,6 +18,14 @@ export declare class MongoWrapper {
18
18
  logWorkflow: any;
19
19
  logCollectIds: string[];
20
20
  debug: any;
21
+ /**
22
+ * Optional hook called after every successful write.
23
+ * Wire to SocketWrapper.emit to push live updates.
24
+ * @param type — "insert" | "update" | "delete"
25
+ * @param id — the document _id as a string ("" for batch operations)
26
+ * @param payload — the written document(s)
27
+ */
28
+ afterWrite?: (type: "insert" | "update" | "delete", id: string, payload: any) => void | Promise<void>;
21
29
  modelChecker: (obj: any) => boolean;
22
30
  constructor(mongoDB: any, collection: string, isProd: boolean, config: SecurityConfig, modelChecker: (obj: any, ignore?: boolean) => boolean, lib: WrapperLib, tableId?: string, logId?: string, logWorkflow?: (data: any) => Promise<any>, logCollectIds?: string[], debug?: (reqBody: any, curBody: any, errMsg: any) => void);
23
31
  parseModel: (data: any) => any;
@@ -1 +1 @@
1
- {"version":3,"file":"mongoWrapper.d.ts","sourceRoot":"","sources":["../../src/apis/mongoWrapper.ts"],"names":[],"mappings":"AASA,OAAO,cAAc,MAAM,yBAAyB,CAAC;AAGrD,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,GAAG,CAAC;CACf;AA6JD;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,EAAE,GAAG,CAAM;IAClB,UAAU,SAAM;IAChB,MAAM,UAAS;IACf,MAAM,EAAE,cAAc,CAQpB;IACF,GAAG,EAAE,UAAU,CAAC;IAChB,OAAO,SAAM;IACb,KAAK,SAAM;IACX,WAAW,EAAE,GAAG,CAAC;IACjB,aAAa,EAAE,MAAM,EAAE,CAAM;IAC7B,KAAK,EAAE,GAAG,CAAC;IAEX,YAAY,GAAI,KAAK,GAAG,aAEtB;gBAGA,OAAO,EAAE,GAAG,EACZ,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,cAAc,EACtB,YAAY,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,EACrD,GAAG,EAAE,UAAU,EACf,OAAO,CAAC,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,EACzC,aAAa,CAAC,EAAE,MAAM,EAAE,EACxB,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,KAAK,IAAI;IAgB3D,UAAU,GAAI,MAAM,GAAG,SAIrB;IAEF,aAAa,GACX,SAAS,GAAG,EACZ,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACpC,KAAK,GAAG,EACR,QAAQ;QACN,GAAG,CAAC,EAAE,GAAG,CAAC;QACV,GAAG,CAAC,EAAE,GAAG,CAAC;KACX,mBA4DD;IAEF,eAAe,GACb,KAAK,GAAG,EACR,KAAK,GAAG,EACR,WAAW,CAAC,QAAQ,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC;;mBAqF5C;IAEF,UAAU,GACR,KAAK,GAAG,EACR,KAAK,GAAG,EACR,WAAW,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;;mBAkDtC;IAEF,OAAO,GAAU,KAAK,GAAG,EAAE,KAAK,GAAG;;mBAuCjC;IAEF,eAAe,GACb,KAAK,GAAG,EACR,KAAK,GAAG,EACR,WAAW,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;;mBAqKtC;IAEF,UAAU,GACR,KAAK,GAAG,EACR,KAAK,GAAG,EACR,WAAW,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;;mBAqGtC;IAEF,eAAe,GACb,KAAK,GAAG,EACR,KAAK,GAAG,EACR,WAAW,CAAC,QAAQ,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC;;mBAsJ5C;IAEF,UAAU,GACR,KAAK,GAAG,EACR,KAAK,GAAG,EACR,WAAW,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;;mBAwCtC;IAEF,mBAAmB,CAAC,GAAG,EAAE,GAAG;IAiT5B,QAAQ,GAAU,KAAK,GAAG,EAAE,KAAK,GAAG;;;;;mBA8ZlC;IAEF,IAAI,YAiFF;CACH"}
1
+ {"version":3,"file":"mongoWrapper.d.ts","sourceRoot":"","sources":["../../src/apis/mongoWrapper.ts"],"names":[],"mappings":"AASA,OAAO,cAAc,MAAM,yBAAyB,CAAC;AAGrD,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,GAAG,CAAC;CACf;AA6JD;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,EAAE,GAAG,CAAM;IAClB,UAAU,SAAM;IAChB,MAAM,UAAS;IACf,MAAM,EAAE,cAAc,CAQpB;IACF,GAAG,EAAE,UAAU,CAAC;IAChB,OAAO,SAAM;IACb,KAAK,SAAM;IACX,WAAW,EAAE,GAAG,CAAC;IACjB,aAAa,EAAE,MAAM,EAAE,CAAM;IAC7B,KAAK,EAAE,GAAG,CAAC;IACX;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,CACX,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACpC,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,GAAG,KACT,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1B,YAAY,GAAI,KAAK,GAAG,aAEtB;gBAGA,OAAO,EAAE,GAAG,EACZ,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,cAAc,EACtB,YAAY,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,EACrD,GAAG,EAAE,UAAU,EACf,OAAO,CAAC,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,MAAM,EACd,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,EACzC,aAAa,CAAC,EAAE,MAAM,EAAE,EACxB,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,KAAK,IAAI;IAgB3D,UAAU,GAAI,MAAM,GAAG,SAIrB;IAEF,aAAa,GACX,SAAS,GAAG,EACZ,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,EACpC,KAAK,GAAG,EACR,QAAQ;QACN,GAAG,CAAC,EAAE,GAAG,CAAC;QACV,GAAG,CAAC,EAAE,GAAG,CAAC;KACX,mBA4DD;IAEF,eAAe,GACb,KAAK,GAAG,EACR,KAAK,GAAG,EACR,WAAW,CAAC,QAAQ,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC;;mBA6F5C;IAEF,UAAU,GACR,KAAK,GAAG,EACR,KAAK,GAAG,EACR,WAAW,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;;mBAyDtC;IAEF,OAAO,GAAU,KAAK,GAAG,EAAE,KAAK,GAAG;;mBAuCjC;IAEF,eAAe,GACb,KAAK,GAAG,EACR,KAAK,GAAG,EACR,WAAW,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;;mBA6KtC;IAEF,UAAU,GACR,KAAK,GAAG,EACR,KAAK,GAAG,EACR,WAAW,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;;mBA4GtC;IAEF,eAAe,GACb,KAAK,GAAG,EACR,KAAK,GAAG,EACR,WAAW,CAAC,QAAQ,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC;;mBAkK5C;IAEF,UAAU,GACR,KAAK,GAAG,EACR,KAAK,GAAG,EACR,WAAW,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC;;mBA+CtC;IAEF,mBAAmB,CAAC,GAAG,EAAE,GAAG;IAiT5B,QAAQ,GAAU,KAAK,GAAG,EAAE,KAAK,GAAG;;;;;mBA8alC;IAEF,IAAI,YAiFF;CACH"}
@@ -1 +1 @@
1
- var __awaiter=this&&this.__awaiter||function(e,r,l,d){return new(l=l||Promise)(function(i,t){function s(e){try{o(d.next(e))}catch(e){t(e)}}function a(e){try{o(d.throw(e))}catch(e){t(e)}}function o(e){var t;e.done?i(e.value):((t=e.value)instanceof l?t:new l(function(e){e(t)})).then(s,a)}o((d=d.apply(e,r||[])).next())})},__importDefault=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(exports,"__esModule",{value:!0}),exports.MongoWrapper=void 0;let dayjs_1=__importDefault(require("./dayjs")),uuid_1=require("uuid"),QueryModel_1=require("../model/QueryModel"),utils_1=require("./utils"),handleParseQueryFilter=(e,d)=>{let n={},u={};return e&&0<e.length&&e.map(i=>{let e,t,s,a=null!=(e=i.queryId)?e:"",o=(d&&(a=d+"."+a),i.value),r={},l=!!i.orQuery;switch(i.type){case"search":i.searchIds&&0<i.searchIds.length&&i.searchIds.map(e=>{l=!0,r[e]={$regex:""+(0,utils_1.handleSpecialWordsEscapeRegex)(o),$options:"i"}}),i.foreign&&0<i.foreign.length&&null!=(t=null==i?void 0:i.foreign)&&t.forEach(e=>{let t=e.alias;d&&(t=d+"."+t),e.searchFields&&0<e.searchFields.length&&e.searchFields.forEach(e=>{r[t+"."+e]={$regex:i.value.toString().replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),$options:"i"}})});break;case"=":r[a]=o;break;case"!=":r[a]={$ne:o};break;case">":r[a]={$gt:o};break;case"<":r[a]={$lt:o};break;case"><":Array.isArray(o)&&2<=o.length&&(r[a]={$gte:o[0],$lte:o[1]});break;case"in":r[a]={$in:o};break;case"!in":r[a]={$nin:o};break;case"exists":r[a]={$exists:o};break;case"elemMatch":r[a]={$elemMatch:o};break;case">size":r.$expr||(r.$expr={}),r.$expr.$gt=[{$size:"$"+a},o];break;case"<size":r.$expr||(r.$expr={}),r.$expr.$lt=[{$size:"$"+a},o]}i.orQueryGroupId&&("search"===i.type?null!=(s=null==i?void 0:i.searchIds)&&s.map(e=>{i.orQueryGroupId&&(u[null==i?void 0:i.orQueryGroupId]?u[null==i?void 0:i.orQueryGroupId].$and.push({[e]:r[e]}):u[null==i?void 0:i.orQueryGroupId]={$and:[{[e]:r[e]}]},delete r[e])}):void 0!==r[a]&&(u[i.orQueryGroupId]?u[i.orQueryGroupId].$and.push({[a]:r[a]}):u[i.orQueryGroupId]={$and:[{[a]:r[a]}]},delete r[a])),Object.keys(r).map(e=>{l?n.$or?n.$or.push({[e]:r[e]}):n.$or=[{[e]:r[e]}]:n[e]=r[e]})}),Object.keys(u).map(e=>{n.$or?n.$or.push(u[e]):n.$or=[u[e]]}),n};class MongoWrapper{constructor(e,t,i,s,a,o,r,l,d,n,u){this.mongoDB="",this.collection="",this.isProd=!1,this.config={baseConfig:[],opsConfig:{read:[],create:[],update:[],delete:[]}},this.tableId="",this.logId="",this.logCollectIds=[],this.modelChecker=e=>!1,this.parseModel=e=>(delete e._id,e.baseUpdatedAt=(0,dayjs_1.default)().toDate(),e),this.handleLogging=(r,l,t,d)=>__awaiter(this,void 0,void 0,function*(){if(this.logId)try{var e=t.body.bm_userId,i=t.body.bm_userRole,o=t.body.bm_userOrg;let s=[],a="create"===l?t.body.data:null==d?void 0:d.new;if(Object.keys(a).map(e=>{var t,i={newValue:a[e],columnId:e};"update"===l&&(i.oldValue=null==(t=null==d?void 0:d.old)?void 0:t[e],JSON.stringify(a[e])===JSON.stringify(null==(t=null==d?void 0:d.old)?void 0:t[e]))||"_id"===e||e.startsWith("base")||s.push(i)}),"update"!==l||0!==s.length){let t={_id:(0,uuid_1.v4)(),type:l,itemId:r,collectionId:this.collection,createdAt:(0,dayjs_1.default)().toDate(),values:s,userId:e,userRole:i,organisationId:o};("create"===l||"update"===l)&&0<this.logCollectIds.length&&this.logCollectIds.forEach(e=>{void 0!==a[e]&&(t[e]=a[e])}),yield this.mongoDB.db(this.tableId).collection(this.logId).insertOne(t),this.logWorkflow&&(yield this.logWorkflow(t))}}catch(e){}}),this.initBatchCreate=(c,h,y)=>__awaiter(this,void 0,void 0,function*(){var e;try{if(Array.isArray(c.body.data)){let t=!0;var a=!!c.body.upsert,o=(c.body.data.map(e=>{this.modelChecker(e)||(t=!1)}),t||h.status(400).json({err:"Invalid Model Structure"}),(0,QueryModel_1.checkBaseConfig)(this.config,c));let i=!0,s=[];if(yield Promise.all(c.body.data.map(e=>{(0,QueryModel_1.checkOpsConfig)(this.config,"update",c,e)||(i=!1),c.body.unique&&null!=e&&e[c.body.unique]&&s.push(e[c.body.unique])})),o&&i){var r=this.mongoDB.db(this.tableId).collection(this.collection);if(0<s.length){var l=yield(yield r.find({[c.body.unique]:{$in:s}})).toArray();if(0<l.length)return void h.status(400).json({err:`Unique:${l.map(e=>e[c.body.unique])} exists`})}let t=[];var d,n=c.body.data.map(e=>(t.push(e._id),Object.assign(Object.assign({},e),{baseUpdatedAt:(0,dayjs_1.default)().toDate()}))),u=(y&&(yield y(n)),a?(d=n.map(e=>({updateOne:{filter:{_id:e._id},update:{$set:e},upsert:!0}})),yield r.bulkWrite(d)):yield r.insertMany(n),{success:t});if(null!=(e=c.body)&&e.stopRes)return u;h.send(u)}else h.status(400).json({err:"Invalid Security Configuration"})}else h.status(400).json({err:"Invalid Fields"})}catch(e){h.status(400).json({err:e})}}),this.initCreate=(l,d,n)=>__awaiter(this,void 0,void 0,function*(){var e,t;try{if(this.modelChecker(l.body.data)){var i=(0,QueryModel_1.checkBaseConfig)(this.config,l),s=(0,QueryModel_1.checkOpsConfig)(this.config,"create",l,l.body.data);if(i&&s){var a=this.mongoDB.db(this.tableId).collection(this.collection);if(l.body.data.baseUpdatedAt=(0,dayjs_1.default)().toDate(),l.body.unique&&null!=(e=l.body.data)&&e[l.body.unique])if(yield a.findOne({[l.body.unique]:l.body.data[l.body.unique]}))return void d.status(400).json({err:`Unique:${l.body.data[l.body.unique]} exists`});n&&(yield n(l.body.data));var o=(yield a.insertOne(l.body.data)).insertedId.toString(),r=(yield this.handleLogging(o,"create",l),{success:!0});if(null!=(t=l.body)&&t.stopRes)return r;d.send(r)}else d.status(400).json({err:"Invalid Security Configuration"})}else d.status(400).json({err:"Invalid Model Structure"})}catch(e){d.status(400).json({err:e})}}),this.initGet=(d,n)=>__awaiter(this,void 0,void 0,function*(){var e;try{var t=d.body.id;if(t){var i,s=this.mongoDB.db(this.tableId).collection(this.collection),a=this.handleBuildPipeline({body:Object.assign(Object.assign({},d.body),{query:[{type:"=",queryId:"_id",value:t}]})}),[o]=yield s.aggregate(a).toArray(),r=(0,QueryModel_1.checkBaseConfig)(this.config,d),l=(0,QueryModel_1.checkOpsConfig)(this.config,"read",d,o);if(r&&l)return i={data:o},null!=(e=d.body)&&e.stopRes?i:void n.send(i);n.status(400).json({err:"Invalid Security Configuration"})}else n.status(400).json({err:"Invalid Fields"})}catch(e){n.status(400).json({err:e})}}),this.initBatchUpdate=($,f,b)=>__awaiter(this,void 0,void 0,function*(){var e,t,r;try{if(this.modelChecker($.body.data)&&$.body.query||$.body.unsetData){var l=$.body.unsetData,d=null!=(e=$.body.query)?e:[];let a=$.body.isOr?"$or":"$and",o={};var n=this.parseModel(null!=(t=$.body.data)?t:[]),u=(d&&0<d.length&&(o[a]=[],d.some(e=>"search"===e.type)&&(o.$or=[]),d.map(e=>{var t,i=null!=(t=e.queryId)?t:"";let s=e.value;switch(e.type){case"search":e.searchIds&&0<e.searchIds.length&&e.searchIds.map(e=>{o.$or.push({[e]:{$regex:(0,utils_1.handleSpecialWordsEscapeRegex)(s),$options:"i"}})});break;case"=":o[a].push({[i]:s});break;case"!=":o[a].push({[i]:{$ne:s}});break;case">":o[a].push({[i]:{$gt:s}});break;case"<":o[a].push({[i]:{$lt:s}});break;case"><":Array.isArray(s)&&2<=s.length&&(o[a]={[i]:{$gte:s[0],$lte:s[1]}});break;case"in":o[a].push({[i]:{$in:s}});break;case"!in":o[a].push({[i]:{$nin:s}});break;case"exists":o[a].push({$exists:s});break;case"elemMatch":o[a].push({[i]:{$elemMatch:s}})}})),(0,QueryModel_1.checkBaseConfig)(this.config,$));let i=!0;var c=this.mongoDB.db(this.tableId).collection(this.collection),h=yield(yield c.find(Object.assign({},o))).toArray();let s=[];if(yield Promise.all(h.map(e=>{$.body.sensitive&&!(0,dayjs_1.default)($.body.data.baseUpdatedAt).isAfter((0,dayjs_1.default)(e.baseUpdatedAt))&&f.status(400).json({err:"Refresh Sensitive Model"}),(0,QueryModel_1.checkOpsConfig)(this.config,"update",$,e)||(i=!1);var t=(0,utils_1.compareUpdatedFields)(e,$.body.data).workflowUpdateFields;s.push(Object.assign(Object.assign({},t),{_id:null==e?void 0:e._id}))})),u&&i){let t={};var y={},p=(l&&(l.forEach(e=>{t[e]=""}),y.$unset=Object.assign({},t)),n&&(y.$set=Object.assign({},n)),b&&(yield b(s)),yield c.updateMany(o,y),s.map(e=>e._id)),g={success:p};if(null!=(r=$.body)&&r.stopRes)return g;f.send(g)}else f.status(400).json({err:"Invalid Security Configuration"})}}catch(e){f.status(400).json({err:e})}}),this.initUpdate=(y,p,g)=>__awaiter(this,void 0,void 0,function*(){var e,s,a;try{var o=this.modelChecker(y.body.data)&&y.body.id;if(o){var r=!!y.body.upsert;let i={};var l=this.mongoDB.db(this.tableId).collection(this.collection),t=(r||(i=yield l.findOne({_id:o})),this.debug&&this.debug(y.body,i),(0,QueryModel_1.checkBaseConfig)(this.config,y)),d=(0,QueryModel_1.checkOpsConfig)(this.config,"update",y,i);if(t&&d){if(y.body.unique&&null!=(e=y.body.data)&&e[y.body.unique]){var n=yield l.findOne({[y.body.unique]:y.body.data[y.body.unique]});if(n&&y.body.id!==n._id)return void p.status(400).json({err:y.body.data[y.body.unique]+" exists"})}var u=(0,utils_1.compareUpdatedFields)(i,y.body.data).updatedFields,c={success:!0},h=(g&&(yield g(Object.assign(Object.assign({},u),{_id:o}))),this.parseModel(u));if(y.body.sensitive)if((0,dayjs_1.default)(y.body.data.baseUpdatedAt).isAfter((0,dayjs_1.default)(i.baseUpdatedAt))){if(yield l.updateOne({_id:o},{$set:Object.assign({},h)},{upsert:r}),null!=(s=y.body)&&s.stopRes)return c;p.send(c)}else p.status(400).json({err:"Refresh Sensitive Model"});else{if(yield l.updateOne({_id:o},{$set:Object.assign({},h)},{upsert:r}),null!=(a=y.body)&&a.stopRes)return c;p.send(c)}let t={};Object.keys(h).map(e=>{t[e]=i[e]}),yield this.handleLogging(o,"update",y,{old:t,new:h})}else p.status(400).json({err:"Invalid Security Configuration"})}else p.status(400).json({err:"Invalid Model Structure"})}catch(e){this.debug&&this.debug(null,null,e),p.status(400).json({err:e})}}),this.initBatchDelete=($,f,b)=>__awaiter(this,void 0,void 0,function*(){var e,t,i,s,a,o;try{if(!(Array.isArray($.body.id)&&0!==(null!=(t=null==(e=$.body.id)?void 0:e.length)?t:0)||Array.isArray($.body.query)&&0!==(null!=(s=null==(i=$.body.query)?void 0:i.length)?s:0)))throw"Invalid Batch Delete Config";if(Array.isArray($.body.id)){var r=this.mongoDB.db(this.tableId).collection(this.collection),l=yield r.find({_id:{$in:$.body.id}}),d=(0,QueryModel_1.checkBaseConfig)(this.config,$),n=yield l.toArray();let t=!0;if(0<n.length&&(yield Promise.all(n.map(e=>{(0,QueryModel_1.checkOpsConfig)(this.config,"delete",$,e)||(t=!1)}))),d&&t){b&&(yield b(n)),yield r.deleteMany({_id:{$in:$.body.id}});var u={success:$.body.id};if(null!=(a=$.body)&&a.stopRes)return u;f.send(u)}else f.status(400).json({err:"Invalid Security Configuration"})}else{var c=$.body.query;let a={};c&&0<c.length&&c.map(e=>{var t,i=null!=(t=e.queryId)?t:"";let s=e.value;switch(e.type){case"search":e.searchIds&&0<e.searchIds.length&&(a.$or=[],e.searchIds.map(e=>{a.$or.push({[e]:{$regex:(0,utils_1.handleSpecialWordsEscapeRegex)(s),$options:"i"}})}));break;case"=":a[i]=s;break;case"!=":a[i]={$ne:s};break;case">":a[i]={$gt:s};break;case"<":a[i]={$lt:s};break;case"><":Array.isArray(s)&&2<=s.length&&(a[i]={$gte:s[0],$lte:s[1]});break;case"in":a[i]={$in:s};break;case"!in":a[i]={$nin:s};break;case"exists":a[i]={$exists:s};break;case"elemMatch":a[i]={$elemMatch:s}}});var h=this.mongoDB.db(this.tableId).collection(this.collection),y=yield h.find(a).toArray(),p=(0,QueryModel_1.checkBaseConfig)(this.config,$);let t=!0,i=[];if(0<y.length&&y.map(e=>{i.push(e._id),(0,QueryModel_1.checkOpsConfig)(this.config,"delete",$,e)||(t=!1)}),b&&(yield b(y)),p&&t){yield h.deleteMany(a);var g={success:i};if(null!=(o=$.body)&&o.stopRes)return g;f.send(g)}else f.status(400).json({err:"Invalid Security Configuration"})}}catch(e){f.status(400).json({err:e})}}),this.initDelete=(l,d,n)=>__awaiter(this,void 0,void 0,function*(){var e;try{var t=l.body.id;if(t){var i=this.mongoDB.db(this.tableId).collection(this.collection),s=yield i.findOne({_id:t}),a=(0,QueryModel_1.checkBaseConfig)(this.config,l),o=(0,QueryModel_1.checkOpsConfig)(this.config,"delete",l,s);if(a&&o){n&&(yield n(s)),yield i.findOneAndDelete({_id:t}),yield this.handleLogging(s._id,"delete",l,{new:s});var r={success:!0};if(null!=(e=l.body)&&e.stopRes)return r;d.send(r)}else d.status(400).json({err:"Invalid Security Configuration"})}else d.status(400).json({err:"Invalid Fields"})}catch(e){d.status(400).json({err:e})}}),this.initList=(ae,oe)=>__awaiter(this,void 0,void 0,function*(){var e,t,i,s,r,l,n,u,c,h,a,y,p,g,$,f,b,v,m,_;try{var I=(0,QueryModel_1.checkBaseConfig)(this.config,ae);if(I){var j=ae.body.aggregate,k=null!=(e=ae.body.unionCollections)?e:[];let o=null!=(t=ae.body.query)?t:[],d;if(d=handleParseQueryFilter(o),k&&Array.isArray(k)&&0<k.length){if(j)return void oe.status(400).json({err:"Union operations cannot be combined with aggregate"});var O=null!=(i=ae.body.sort)?i:{date:-1},M=null!=(s=ae.body.limit)?s:50,C=null!=(l=null!=(r=ae.body.cursor)?r:ae.body.skip)?l:0;let a=[];if(k.forEach((e,t)=>{let i={$project:Object.assign({_id:1},e.projection)};var s;e.sourceLogic?i.$project.source="string"==typeof e.sourceLogic?{$literal:e.sourceLogic}:e.sourceLogic:i.$project.source={$literal:e.source},0===t?(o&&0<o.length&&a.push({$match:d}),e.query&&0<e.query.length&&(t=handleParseQueryFilter(e.query),a.push({$match:t})),e.addFields&&(a.push({$addFields:e.addFields}),Object.keys(e.addFields).forEach(e=>{i.$project[e]=1})),a.push(i)):(t=[],e.query&&0<e.query.length&&(s=handleParseQueryFilter(e.query),t.push({$match:s})),e.addFields&&(t.push({$addFields:e.addFields}),Object.keys(e.addFields).forEach(e=>{i.$project[e]=1})),t.push(i),s={$unionWith:{coll:e.collection,pipeline:t}},a.push(s))}),O){let t={$sort:{}};Array.isArray(O)?O.forEach(e=>{e.sortId&&(t.$sort[e.sortId]="asc"===e.type?1:-1)}):O.sortId&&O.type?t.$sort[O.sortId]="asc"===O.type?1:-1:t.$sort=O,a.push(t)}0<C&&a.push({$skip:C}),0<M&&a.push({$limit:M});var A,S=null!=(u=null==(n=k[0])?void 0:n.collection)?u:this.collection,D=this.mongoDB.db(this.tableId).collection(S),q=yield D.aggregate(a).toArray(),w=a.filter(e=>{e=Object.keys(e)[0];return"$skip"!==e&&"$limit"!==e}),E=(w.push({$count:"total"}),yield D.aggregate(w).toArray()),U=(null==(c=E[0])?void 0:c.total)||0;let t=!0;return(q.map(e=>{(0,QueryModel_1.checkOpsConfig)(this.config,"read",ae,e)||(t=!1)}),I&&t)?(A={data:q,count:U},null!=(h=ae.body)&&h.stopRes?A:void oe.send(A)):void oe.status(400).json({err:"Invalid Security Configuration"})}if(j){var Y,H=[],P=null!=(a=j.queryId)?a:"",L=null!=(y=j.dateId)?y:"baseUpdatedAt",F=j.timezone||"Asia/Kuala_Lumpur";for(Y of null!=(p=j.range)?p:[(0,dayjs_1.default)().tz(F).format("DD/MM/YYYY HH:mm:ss")]){Object.assign({},d);var W=Y.split("-"),[Q,z=(0,dayjs_1.default)().tz(F).format("DD/MM/YYYY HH:mm:ss")]=W,G=2===W.length,T=(0,dayjs_1.default)(Q,"DD/MM/YYYY HH:mm:ss").isValid(),N=(0,dayjs_1.default)(z,"DD/MM/YYYY HH:mm:ss").isValid(),V=dayjs_1.default.tz((0,dayjs_1.default)(Q,"DD/MM/YYYY HH:mm:ss").format("YYYY-MM-DDTHH:mm:ss"),F).utc().toDate(),J=dayjs_1.default.tz((0,dayjs_1.default)(z,"DD/MM/YYYY HH:mm:ss").format("YYYY-MM-DDTHH:mm:ss"),F).utc().toDate();if(!T||!N)return void oe.status(400).json({err:"Invalid Aggregate Range Configuration"});let o=[G?{$match:Object.assign(Object.assign({},d),{[L]:{$gte:V,$lte:J}})}:{$match:Object.assign(Object.assign({},d),{[L]:{$lte:V}})}],r=G?Y:"Begining until "+(0,dayjs_1.default)(Q,"DD/MM/YYYY HH:mm:ss").format("DD/MM/YYYY"),l=!1;if(j.unwind&&0<j.unwind.length&&j.unwind.map((e,t)=>{var i,s="model"+t;let a="";0<t&&(a="model"+(t-1)+".");t=handleParseQueryFilter(null!=(t=e.query)?t:[],s);null!=(i=e.unique)&&i.list&&o.push({$unwind:"$"+e.localField}),o.push({$lookup:{from:e.collectionId,localField:a+e.localField,foreignField:"_id",as:s}},{$unwind:"$"+s},{$match:t}),null!=(i=e.unique)&&i.type&&(l=!0,s="uniqueUnwindId",o.push({$group:{_id:r,uniqueUnwindId:{$addToSet:"$"+e.localField}}}),"count"===e.unique.type?o.push({$project:{value:{$size:"$"+s}}}):o.push({$project:{_id:0,value:"$"+s}}))}),!l){let e=r;switch(j.groupId&&(e="$"+j.groupId),j.type){case"avg":o.push({$group:{_id:e,value:{$avg:"$"+P}}});break;case"sum":o.push({$group:{_id:e,value:{$sum:"$"+P}}});break;case"count":o.push({$group:{_id:e,value:{$sum:1},ids:{$push:"$_id"}}})}}if(0===o.length)return void oe.status(400).json({err:"Invalid Aggregate Configuration"});var K=this.mongoDB.db(this.tableId).collection(this.collection);let e=0,t=[];var B=yield K.aggregate(o).toArray();j.groupId?e=B:(e=null!=($=null==(g=B[0])?void 0:g.value)?$:0,t=null!=(b=null==(f=B[0])?void 0:f.ids)?b:[]),H.push({_id:r,value:e,ids:t})}var X={data:H};if(null!=(v=ae.body)&&v.stopRes)return X;oe.send(X)}else{var R,Z=this.mongoDB.db(this.tableId).collection(this.collection),ee=this.handleBuildPipeline(ae),te=ee.filter(e=>{e=Object.keys(e)[0];return"$skip"!==e&&"$limit"!==e}),[x,ie]=(te.push({$count:"total"}),yield Promise.all([Z.aggregate(ee).toArray(),Z.aggregate(te).toArray()])),se=(null==(m=ie[0])?void 0:m.total)||0;let t=!0;if(0<x.length&&x.map(e=>{(0,QueryModel_1.checkOpsConfig)(this.config,"read",ae,e)||(t=!1)}),I&&t)return R={data:x,count:se},null!=(_=ae.body)&&_.stopRes?R:void oe.send(R);oe.status(400).json({err:"Invalid Security Configuration"})}}else oe.status(400).json({err:"Invalid Security Configuration"})}catch(e){oe.status(400).json({err:e.message})}}),this.init=()=>{var e=this.lib.express.Router();return e.post("/create",(e,t)=>{this.initCreate(e,t)}),e.post("/batchCreate",(e,t)=>{this.initBatchCreate(e,t)}),e.post("/get",(e,t)=>{this.initGet(e,t)}),e.post("/update",(e,t)=>{this.initUpdate(e,t)}),e.post("/batchUpdate",(e,t)=>{this.initBatchUpdate(e,t)}),e.post("/delete",(e,t)=>{this.initDelete(e,t)}),e.post("/batchDelete",(e,t)=>{this.initBatchDelete(e,t)}),e.post("/list",(e,t)=>{this.initList(e,t)}),e},this.mongoDB=e,this.collection=t,this.isProd=i,this.config=s,this.modelChecker=a,this.lib=o;e=this.isProd?"prod":"dev";this.tableId=r?r+"-"+e:e,this.logId=null!=l?l:"",this.logWorkflow=d,this.logCollectIds=null!=n?n:[],this.debug=u}handleBuildPipeline(e){let n=[],i=new Map,u=new Set;var{query:s,sort:a,cursor:o,limit:r,stopLimit:l,pipeline:d,uniqueLabel:c,groupId:h}=e.body;try{Array.isArray(d)&&0<d.length&&d.forEach(e=>{var{condition:e,referenceSpaceLabel:s,spaceReferenceId:a,spaceReferenceAs:o,referenceSort:r,referenceFirstItem:l,referenceLimit:d}=e;if(a){let i=o||"linkedData",t=(u.add(i),{$lookup:{from:a,let:{},pipeline:[],as:i}});if(e&&0<e.length){e.forEach(e=>{t.$lookup.let["local_"+e.fromColumnId]="$"+e.fromColumnId});let i={$match:{$expr:{$and:[]}}};e.forEach(e=>{var t={};switch(e.type){case"=":t.$eq=["$"+e.toColumnId,"$$local_"+e.fromColumnId];break;case"!=":t.$ne=["$"+e.toColumnId,"$$local_"+e.fromColumnId];break;case">":t.$gt=["$"+e.toColumnId,"$$local_"+e.fromColumnId];break;case"<":t.$lt=["$"+e.toColumnId,"$$local_"+e.fromColumnId];break;case"in":t.$in=["$"+e.toColumnId,"$$local_"+e.fromColumnId];break;case"!in":t.$nin=["$"+e.toColumnId,"$$local_"+e.fromColumnId]}0<Object.keys(t).length&&i.$match.$expr.$and.push(t)}),t.$lookup.pipeline.push(i)}if(r&&r.sortId&&(o={$sort:{[r.sortId]:"asc"===r.type?1:-1}},t.$lookup.pipeline.push(o)),d&&t.$lookup.pipeline.push({$limit:d}),n.push(t),s&&0<s.length){let t={$addFields:{}};l?(n.push({$addFields:{[i]:{$first:{$ifNull:["$"+i,[]]}}}}),s.forEach(e=>{t.$addFields[i+"_"+e]=`$${i}.`+e})):s.forEach(e=>{t.$addFields[i+"_"+e]={$map:{input:"$"+i,in:"$$this."+e}}}),n.push(t)}}}),s&&0<s.length&&s.forEach(e=>{e.foreign&&Array.isArray(e.foreign)&&e.foreign.forEach(e=>{var t=e.alias;i.has(t)||(n.push({$lookup:{from:e.collection,localField:e.localField,foreignField:"_id",as:t}}),i.set(t,!0))})});let t={$sort:{}};a&&(Array.isArray(JSON.parse(JSON.stringify(a)))?a:[a]).forEach(e=>{e.sortId&&(t.$sort[e.sortId]="asc"===e.type?1:-1)});var y,p=null==a?void 0:a.sortBeforeFilter;if(p&&(0<Object.keys(t.$sort).length&&n.push(t),c)&&(n.push({$group:{_id:"$"+c,doc:{$first:"$$ROOT"}}}),n.push({$replaceRoot:{newRoot:"$doc"}})),s&&0<s.length&&(y=handleParseQueryFilter(s),n.push({$match:y})),p||(0<Object.keys(t.$sort).length&&n.push(t),c&&(n.push({$group:{_id:"$"+c,doc:{$first:"$$ROOT"}}}),n.push({$replaceRoot:{newRoot:"$doc"}}))),h&&!c&&(n.push({$group:{_id:"$"+h,topItem:{$first:"$$ROOT"},children:{$push:"$$ROOT"}}}),n.push({$replaceRoot:{newRoot:{$mergeObjects:["$topItem",{children:{$cond:{if:{$gt:[{$size:"$children"},0]},then:"$children",else:"$$REMOVE"}}}]}}}),0<Object.keys(t.$sort).length)&&n.push(t),e.body.attributeIds&&0<e.body.attributeIds.length){let t={_id:1};e.body.attributeIds.forEach(e=>{t[e]=1}),n.push({$project:t})}e.body.excludeAttributeIds&&0<e.body.excludeAttributeIds.length&&n.push({$unset:e.body.excludeAttributeIds}),o&&n.push({$skip:Number.parseInt(o)}),l||n.push({$limit:r||10});var g=Array.from(i.keys()),$=(0<g.length&&n.push({$unset:[...g]}),Array.from(u));0<$.length&&n.push({$unset:[...$]})}catch(e){}return n}}exports.MongoWrapper=MongoWrapper;
1
+ var __awaiter=this&&this.__awaiter||function(e,o,l,d){return new(l=l||Promise)(function(i,t){function s(e){try{a(d.next(e))}catch(e){t(e)}}function r(e){try{a(d.throw(e))}catch(e){t(e)}}function a(e){var t;e.done?i(e.value):((t=e.value)instanceof l?t:new l(function(e){e(t)})).then(s,r)}a((d=d.apply(e,o||[])).next())})},__importDefault=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(exports,"__esModule",{value:!0}),exports.MongoWrapper=void 0;let dayjs_1=__importDefault(require("./dayjs")),uuid_1=require("uuid"),QueryModel_1=require("../model/QueryModel"),utils_1=require("./utils"),handleParseQueryFilter=(e,d)=>{let n={},u={};return e&&0<e.length&&e.map(i=>{let e,t,s,r=null!=(e=i.queryId)?e:"",a=(d&&(r=d+"."+r),i.value),o={},l=!!i.orQuery;switch(i.type){case"search":i.searchIds&&0<i.searchIds.length&&i.searchIds.map(e=>{l=!0,o[e]={$regex:""+(0,utils_1.handleSpecialWordsEscapeRegex)(a),$options:"i"}}),i.foreign&&0<i.foreign.length&&null!=(t=null==i?void 0:i.foreign)&&t.forEach(e=>{let t=e.alias;d&&(t=d+"."+t),e.searchFields&&0<e.searchFields.length&&e.searchFields.forEach(e=>{o[t+"."+e]={$regex:i.value.toString().replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),$options:"i"}})});break;case"=":o[r]=a;break;case"!=":o[r]={$ne:a};break;case">":o[r]={$gt:a};break;case"<":o[r]={$lt:a};break;case"><":Array.isArray(a)&&2<=a.length&&(o[r]={$gte:a[0],$lte:a[1]});break;case"in":o[r]={$in:a};break;case"!in":o[r]={$nin:a};break;case"exists":o[r]={$exists:a};break;case"elemMatch":o[r]={$elemMatch:a};break;case">size":o.$expr||(o.$expr={}),o.$expr.$gt=[{$size:"$"+r},a];break;case"<size":o.$expr||(o.$expr={}),o.$expr.$lt=[{$size:"$"+r},a]}i.orQueryGroupId&&("search"===i.type?null!=(s=null==i?void 0:i.searchIds)&&s.map(e=>{i.orQueryGroupId&&(u[null==i?void 0:i.orQueryGroupId]?u[null==i?void 0:i.orQueryGroupId].$and.push({[e]:o[e]}):u[null==i?void 0:i.orQueryGroupId]={$and:[{[e]:o[e]}]},delete o[e])}):void 0!==o[r]&&(u[i.orQueryGroupId]?u[i.orQueryGroupId].$and.push({[r]:o[r]}):u[i.orQueryGroupId]={$and:[{[r]:o[r]}]},delete o[r])),Object.keys(o).map(e=>{l?n.$or?n.$or.push({[e]:o[e]}):n.$or=[{[e]:o[e]}]:n[e]=o[e]})}),Object.keys(u).map(e=>{n.$or?n.$or.push(u[e]):n.$or=[u[e]]}),n};class MongoWrapper{constructor(e,t,i,s,r,a,o,l,d,n,u){this.mongoDB="",this.collection="",this.isProd=!1,this.config={baseConfig:[],opsConfig:{read:[],create:[],update:[],delete:[]}},this.tableId="",this.logId="",this.logCollectIds=[],this.modelChecker=e=>!1,this.parseModel=e=>(delete e._id,e.baseUpdatedAt=(0,dayjs_1.default)().toDate(),e),this.handleLogging=(o,l,t,d)=>__awaiter(this,void 0,void 0,function*(){if(this.logId)try{var e=t.body.bm_userId,i=t.body.bm_userRole,a=t.body.bm_userOrg;let s=[],r="create"===l?t.body.data:null==d?void 0:d.new;if(Object.keys(r).map(e=>{var t,i={newValue:r[e],columnId:e};"update"===l&&(i.oldValue=null==(t=null==d?void 0:d.old)?void 0:t[e],JSON.stringify(r[e])===JSON.stringify(null==(t=null==d?void 0:d.old)?void 0:t[e]))||"_id"===e||e.startsWith("base")||s.push(i)}),"update"!==l||0!==s.length){let t={_id:(0,uuid_1.v4)(),type:l,itemId:o,collectionId:this.collection,createdAt:(0,dayjs_1.default)().toDate(),values:s,userId:e,userRole:i,organisationId:a};("create"===l||"update"===l)&&0<this.logCollectIds.length&&this.logCollectIds.forEach(e=>{void 0!==r[e]&&(t[e]=r[e])}),yield this.mongoDB.db(this.tableId).collection(this.logId).insertOne(t),this.logWorkflow&&(yield this.logWorkflow(t))}}catch(e){}}),this.initBatchCreate=(c,h,y)=>__awaiter(this,void 0,void 0,function*(){var e;try{if(Array.isArray(c.body.data)){let t=!0;var r=!!c.body.upsert,a=(c.body.data.map(e=>{this.modelChecker(e)||(t=!1)}),t||h.status(400).json({err:"Invalid Model Structure"}),(0,QueryModel_1.checkBaseConfig)(this.config,c));let i=!0,s=[];if(yield Promise.all(c.body.data.map(e=>{(0,QueryModel_1.checkOpsConfig)(this.config,"update",c,e)||(i=!1),c.body.unique&&null!=e&&e[c.body.unique]&&s.push(e[c.body.unique])})),a&&i){var o=this.mongoDB.db(this.tableId).collection(this.collection);if(0<s.length){var l=yield(yield o.find({[c.body.unique]:{$in:s}})).toArray();if(0<l.length)return void h.status(400).json({err:`Unique:${l.map(e=>e[c.body.unique])} exists`})}let t=[];var d,n=c.body.data.map(e=>(t.push(e._id),Object.assign(Object.assign({},e),{baseUpdatedAt:(0,dayjs_1.default)().toDate()}))),u=(y&&(yield y(n)),r?(d=n.map(e=>({updateOne:{filter:{_id:e._id},update:{$set:e},upsert:!0}})),yield o.bulkWrite(d)):yield o.insertMany(n),this.afterWrite&&Promise.resolve(this.afterWrite("insert","",n)).catch(e=>console.error("[MongoWrapper] afterWrite failed",e)),{success:t});if(null!=(e=c.body)&&e.stopRes)return u;h.send(u)}else h.status(400).json({err:"Invalid Security Configuration"})}else h.status(400).json({err:"Invalid Fields"})}catch(e){h.status(400).json({err:e})}}),this.initCreate=(l,d,n)=>__awaiter(this,void 0,void 0,function*(){var e,t;try{if(this.modelChecker(l.body.data)){var i=(0,QueryModel_1.checkBaseConfig)(this.config,l),s=(0,QueryModel_1.checkOpsConfig)(this.config,"create",l,l.body.data);if(i&&s){var r=this.mongoDB.db(this.tableId).collection(this.collection);if(l.body.data.baseUpdatedAt=(0,dayjs_1.default)().toDate(),l.body.unique&&null!=(e=l.body.data)&&e[l.body.unique])if(yield r.findOne({[l.body.unique]:l.body.data[l.body.unique]}))return void d.status(400).json({err:`Unique:${l.body.data[l.body.unique]} exists`});n&&(yield n(l.body.data));var a=(yield r.insertOne(l.body.data)).insertedId.toString(),o=(yield this.handleLogging(a,"create",l),this.afterWrite&&Promise.resolve(this.afterWrite("insert",a,l.body.data)).catch(e=>console.error("[MongoWrapper] afterWrite failed",e)),{success:!0});if(null!=(t=l.body)&&t.stopRes)return o;d.send(o)}else d.status(400).json({err:"Invalid Security Configuration"})}else d.status(400).json({err:"Invalid Model Structure"})}catch(e){d.status(400).json({err:e})}}),this.initGet=(d,n)=>__awaiter(this,void 0,void 0,function*(){var e;try{var t=d.body.id;if(t){var i,s=this.mongoDB.db(this.tableId).collection(this.collection),r=this.handleBuildPipeline({body:Object.assign(Object.assign({},d.body),{query:[{type:"=",queryId:"_id",value:t}]})}),[a]=yield s.aggregate(r).toArray(),o=(0,QueryModel_1.checkBaseConfig)(this.config,d),l=(0,QueryModel_1.checkOpsConfig)(this.config,"read",d,a);if(o&&l)return i={data:a},null!=(e=d.body)&&e.stopRes?i:void n.send(i);n.status(400).json({err:"Invalid Security Configuration"})}else n.status(400).json({err:"Invalid Fields"})}catch(e){n.status(400).json({err:e})}}),this.initBatchUpdate=(f,$,b)=>__awaiter(this,void 0,void 0,function*(){var e,t,o;try{if(this.modelChecker(f.body.data)&&f.body.query||f.body.unsetData){var l=f.body.unsetData,d=null!=(e=f.body.query)?e:[];let r=f.body.isOr?"$or":"$and",a={};var n=this.parseModel(null!=(t=f.body.data)?t:[]),u=(d&&0<d.length&&(a[r]=[],d.some(e=>"search"===e.type)&&(a.$or=[]),d.map(e=>{var t,i=null!=(t=e.queryId)?t:"";let s=e.value;switch(e.type){case"search":e.searchIds&&0<e.searchIds.length&&e.searchIds.map(e=>{a.$or.push({[e]:{$regex:(0,utils_1.handleSpecialWordsEscapeRegex)(s),$options:"i"}})});break;case"=":a[r].push({[i]:s});break;case"!=":a[r].push({[i]:{$ne:s}});break;case">":a[r].push({[i]:{$gt:s}});break;case"<":a[r].push({[i]:{$lt:s}});break;case"><":Array.isArray(s)&&2<=s.length&&(a[r]={[i]:{$gte:s[0],$lte:s[1]}});break;case"in":a[r].push({[i]:{$in:s}});break;case"!in":a[r].push({[i]:{$nin:s}});break;case"exists":a[r].push({$exists:s});break;case"elemMatch":a[r].push({[i]:{$elemMatch:s}})}})),(0,QueryModel_1.checkBaseConfig)(this.config,f));let i=!0;var c=this.mongoDB.db(this.tableId).collection(this.collection),h=yield(yield c.find(Object.assign({},a))).toArray();let s=[];if(yield Promise.all(h.map(e=>{f.body.sensitive&&!(0,dayjs_1.default)(f.body.data.baseUpdatedAt).isAfter((0,dayjs_1.default)(e.baseUpdatedAt))&&$.status(400).json({err:"Refresh Sensitive Model"}),(0,QueryModel_1.checkOpsConfig)(this.config,"update",f,e)||(i=!1);var t=(0,utils_1.compareUpdatedFields)(e,f.body.data).workflowUpdateFields;s.push(Object.assign(Object.assign({},t),{_id:null==e?void 0:e._id}))})),u&&i){let t={};var y={},p=(l&&(l.forEach(e=>{t[e]=""}),y.$unset=Object.assign({},t)),n&&(y.$set=Object.assign({},n)),b&&(yield b(s)),yield c.updateMany(a,y),this.afterWrite&&Promise.resolve(this.afterWrite("update","",s)).catch(e=>console.error("[MongoWrapper] afterWrite failed",e)),s.map(e=>e._id)),g={success:p};if(null!=(o=f.body)&&o.stopRes)return g;$.send(g)}else $.status(400).json({err:"Invalid Security Configuration"})}}catch(e){$.status(400).json({err:e})}}),this.initUpdate=(y,p,g)=>__awaiter(this,void 0,void 0,function*(){var e,s,r;try{var a=this.modelChecker(y.body.data)&&y.body.id;if(a){var o=!!y.body.upsert;let i={};var l=this.mongoDB.db(this.tableId).collection(this.collection),t=(o||(i=yield l.findOne({_id:a})),this.debug&&this.debug(y.body,i),(0,QueryModel_1.checkBaseConfig)(this.config,y)),d=(0,QueryModel_1.checkOpsConfig)(this.config,"update",y,i);if(t&&d){if(y.body.unique&&null!=(e=y.body.data)&&e[y.body.unique]){var n=yield l.findOne({[y.body.unique]:y.body.data[y.body.unique]});if(n&&y.body.id!==n._id)return void p.status(400).json({err:y.body.data[y.body.unique]+" exists"})}var u=(0,utils_1.compareUpdatedFields)(i,y.body.data).updatedFields,c={success:!0},h=(g&&(yield g(Object.assign(Object.assign({},u),{_id:a}))),this.parseModel(u));if(y.body.sensitive)if((0,dayjs_1.default)(y.body.data.baseUpdatedAt).isAfter((0,dayjs_1.default)(i.baseUpdatedAt))){if(yield l.updateOne({_id:a},{$set:Object.assign({},h)},{upsert:o}),null!=(s=y.body)&&s.stopRes)return c;p.send(c)}else p.status(400).json({err:"Refresh Sensitive Model"});else{if(yield l.updateOne({_id:a},{$set:Object.assign({},h)},{upsert:o}),null!=(r=y.body)&&r.stopRes)return c;p.send(c)}let t={};Object.keys(h).map(e=>{t[e]=i[e]}),yield this.handleLogging(a,"update",y,{old:t,new:h}),this.afterWrite&&Promise.resolve(this.afterWrite("update",String(a),Object.assign({_id:a},h))).catch(e=>console.error("[MongoWrapper] afterWrite failed",e))}else p.status(400).json({err:"Invalid Security Configuration"})}else p.status(400).json({err:"Invalid Model Structure"})}catch(e){this.debug&&this.debug(null,null,e),p.status(400).json({err:e})}}),this.initBatchDelete=(f,$,b)=>__awaiter(this,void 0,void 0,function*(){var e,t,i,s,r,a;try{if(!(Array.isArray(f.body.id)&&0!==(null!=(t=null==(e=f.body.id)?void 0:e.length)?t:0)||Array.isArray(f.body.query)&&0!==(null!=(s=null==(i=f.body.query)?void 0:i.length)?s:0)))throw"Invalid Batch Delete Config";if(Array.isArray(f.body.id)){var o=this.mongoDB.db(this.tableId).collection(this.collection),l=yield o.find({_id:{$in:f.body.id}}),d=(0,QueryModel_1.checkBaseConfig)(this.config,f),n=yield l.toArray();let t=!0;if(0<n.length&&(yield Promise.all(n.map(e=>{(0,QueryModel_1.checkOpsConfig)(this.config,"delete",f,e)||(t=!1)}))),d&&t){b&&(yield b(n)),yield o.deleteMany({_id:{$in:f.body.id}}),this.afterWrite&&Promise.resolve(this.afterWrite("delete","",n)).catch(e=>console.error("[MongoWrapper] afterWrite failed",e));var u={success:f.body.id};if(null!=(r=f.body)&&r.stopRes)return u;$.send(u)}else $.status(400).json({err:"Invalid Security Configuration"})}else{var c=f.body.query;let r={};c&&0<c.length&&c.map(e=>{var t,i=null!=(t=e.queryId)?t:"";let s=e.value;switch(e.type){case"search":e.searchIds&&0<e.searchIds.length&&(r.$or=[],e.searchIds.map(e=>{r.$or.push({[e]:{$regex:(0,utils_1.handleSpecialWordsEscapeRegex)(s),$options:"i"}})}));break;case"=":r[i]=s;break;case"!=":r[i]={$ne:s};break;case">":r[i]={$gt:s};break;case"<":r[i]={$lt:s};break;case"><":Array.isArray(s)&&2<=s.length&&(r[i]={$gte:s[0],$lte:s[1]});break;case"in":r[i]={$in:s};break;case"!in":r[i]={$nin:s};break;case"exists":r[i]={$exists:s};break;case"elemMatch":r[i]={$elemMatch:s}}});var h=this.mongoDB.db(this.tableId).collection(this.collection),y=yield h.find(r).toArray(),p=(0,QueryModel_1.checkBaseConfig)(this.config,f);let t=!0,i=[];if(0<y.length&&y.map(e=>{i.push(e._id),(0,QueryModel_1.checkOpsConfig)(this.config,"delete",f,e)||(t=!1)}),b&&(yield b(y)),p&&t){yield h.deleteMany(r),this.afterWrite&&Promise.resolve(this.afterWrite("delete","",y)).catch(e=>console.error("[MongoWrapper] afterWrite failed",e));var g={success:i};if(null!=(a=f.body)&&a.stopRes)return g;$.send(g)}else $.status(400).json({err:"Invalid Security Configuration"})}}catch(e){$.status(400).json({err:e})}}),this.initDelete=(l,d,n)=>__awaiter(this,void 0,void 0,function*(){var e;try{var t=l.body.id;if(t){var i=this.mongoDB.db(this.tableId).collection(this.collection),s=yield i.findOne({_id:t}),r=(0,QueryModel_1.checkBaseConfig)(this.config,l),a=(0,QueryModel_1.checkOpsConfig)(this.config,"delete",l,s);if(r&&a){n&&(yield n(s)),yield i.findOneAndDelete({_id:t}),yield this.handleLogging(s._id,"delete",l,{new:s}),this.afterWrite&&Promise.resolve(this.afterWrite("delete",String(s._id),s)).catch(e=>console.error("[MongoWrapper] afterWrite failed",e));var o={success:!0};if(null!=(e=l.body)&&e.stopRes)return o;d.send(o)}else d.status(400).json({err:"Invalid Security Configuration"})}else d.status(400).json({err:"Invalid Fields"})}catch(e){d.status(400).json({err:e})}}),this.initList=(re,ae)=>__awaiter(this,void 0,void 0,function*(){var e,t,i,s,o,l,n,u,c,h,r,y,p,g,f,$,b,v,m,_;try{var I=(0,QueryModel_1.checkBaseConfig)(this.config,re);if(I){var j=re.body.aggregate,k=null!=(e=re.body.unionCollections)?e:[];let a=null!=(t=re.body.query)?t:[],d;if(d=handleParseQueryFilter(a),k&&Array.isArray(k)&&0<k.length){if(j)return void ae.status(400).json({err:"Union operations cannot be combined with aggregate"});var M=null!=(i=re.body.sort)?i:{date:-1},O=null!=(s=re.body.limit)?s:50,C=null!=(l=null!=(o=re.body.cursor)?o:re.body.skip)?l:0;let r=[];if(k.forEach((e,t)=>{let i={$project:Object.assign({_id:1},e.projection)};var s;e.sourceLogic?i.$project.source="string"==typeof e.sourceLogic?{$literal:e.sourceLogic}:e.sourceLogic:i.$project.source={$literal:e.source},0===t?(a&&0<a.length&&r.push({$match:d}),e.query&&0<e.query.length&&(t=handleParseQueryFilter(e.query),r.push({$match:t})),e.addFields&&(r.push({$addFields:e.addFields}),Object.keys(e.addFields).forEach(e=>{i.$project[e]=1})),r.push(i)):(t=[],e.query&&0<e.query.length&&(s=handleParseQueryFilter(e.query),t.push({$match:s})),e.addFields&&(t.push({$addFields:e.addFields}),Object.keys(e.addFields).forEach(e=>{i.$project[e]=1})),t.push(i),s={$unionWith:{coll:e.collection,pipeline:t}},r.push(s))}),M){let t={$sort:{}};Array.isArray(M)?M.forEach(e=>{e.sortId&&(t.$sort[e.sortId]="asc"===e.type?1:-1)}):M.sortId&&M.type?t.$sort[M.sortId]="asc"===M.type?1:-1:t.$sort=M,r.push(t)}0<C&&r.push({$skip:C}),0<O&&r.push({$limit:O});var A,x=null!=(u=null==(n=k[0])?void 0:n.collection)?u:this.collection,D=this.mongoDB.db(this.tableId).collection(x),q=yield D.aggregate(r).toArray(),w=r.filter(e=>{e=Object.keys(e)[0];return"$skip"!==e&&"$limit"!==e}),S=(w.push({$count:"total"}),yield D.aggregate(w).toArray()),P=(null==(c=S[0])?void 0:c.total)||0;let t=!0;return(q.map(e=>{(0,QueryModel_1.checkOpsConfig)(this.config,"read",re,e)||(t=!1)}),I&&t)?(A={data:q,count:P},null!=(h=re.body)&&h.stopRes?A:void ae.send(A)):void ae.status(400).json({err:"Invalid Security Configuration"})}if(j){var W,E=[],U=null!=(r=j.queryId)?r:"",H=null!=(y=j.dateId)?y:"baseUpdatedAt",Y=j.timezone||"Asia/Kuala_Lumpur";for(W of null!=(p=j.range)?p:[(0,dayjs_1.default)().tz(Y).format("DD/MM/YYYY HH:mm:ss")]){Object.assign({},d);var L=W.split("-"),[F,z=(0,dayjs_1.default)().tz(Y).format("DD/MM/YYYY HH:mm:ss")]=L,G=2===L.length,T=(0,dayjs_1.default)(F,"DD/MM/YYYY HH:mm:ss").isValid(),N=(0,dayjs_1.default)(z,"DD/MM/YYYY HH:mm:ss").isValid(),V=dayjs_1.default.tz((0,dayjs_1.default)(F,"DD/MM/YYYY HH:mm:ss").format("YYYY-MM-DDTHH:mm:ss"),Y).utc().toDate(),J=dayjs_1.default.tz((0,dayjs_1.default)(z,"DD/MM/YYYY HH:mm:ss").format("YYYY-MM-DDTHH:mm:ss"),Y).utc().toDate();if(!T||!N)return void ae.status(400).json({err:"Invalid Aggregate Range Configuration"});let a=[G?{$match:Object.assign(Object.assign({},d),{[H]:{$gte:V,$lte:J}})}:{$match:Object.assign(Object.assign({},d),{[H]:{$lte:V}})}],o=G?W:"Begining until "+(0,dayjs_1.default)(F,"DD/MM/YYYY HH:mm:ss").format("DD/MM/YYYY"),l=!1;if(j.unwind&&0<j.unwind.length&&j.unwind.map((e,t)=>{var i,s="model"+t;let r="";0<t&&(r="model"+(t-1)+".");t=handleParseQueryFilter(null!=(t=e.query)?t:[],s);null!=(i=e.unique)&&i.list&&a.push({$unwind:"$"+e.localField}),a.push({$lookup:{from:e.collectionId,localField:r+e.localField,foreignField:"_id",as:s}},{$unwind:"$"+s},{$match:t}),null!=(i=e.unique)&&i.type&&(l=!0,s="uniqueUnwindId",a.push({$group:{_id:o,uniqueUnwindId:{$addToSet:"$"+e.localField}}}),"count"===e.unique.type?a.push({$project:{value:{$size:"$"+s}}}):a.push({$project:{_id:0,value:"$"+s}}))}),!l){let e=o;switch(j.groupId&&(e="$"+j.groupId),j.type){case"avg":a.push({$group:{_id:e,value:{$avg:"$"+U}}});break;case"sum":a.push({$group:{_id:e,value:{$sum:"$"+U}}});break;case"count":a.push({$group:{_id:e,value:{$sum:1},ids:{$push:"$_id"}}})}}if(0===a.length)return void ae.status(400).json({err:"Invalid Aggregate Configuration"});var K=this.mongoDB.db(this.tableId).collection(this.collection);let e=0,t=[];var Q=yield K.aggregate(a).toArray();j.groupId?e=Q:(e=null!=(f=null==(g=Q[0])?void 0:g.value)?f:0,t=null!=(b=null==($=Q[0])?void 0:$.ids)?b:[]),E.push({_id:o,value:e,ids:t})}var X={data:E};if(null!=(v=re.body)&&v.stopRes)return X;ae.send(X)}else{var B,Z=this.mongoDB.db(this.tableId).collection(this.collection),ee=this.handleBuildPipeline(re),te=ee.filter(e=>{e=Object.keys(e)[0];return"$skip"!==e&&"$limit"!==e}),[R,ie]=(te.push({$count:"total"}),yield Promise.all([Z.aggregate(ee).toArray(),Z.aggregate(te).toArray()])),se=(null==(m=ie[0])?void 0:m.total)||0;let t=!0;if(0<R.length&&R.map(e=>{(0,QueryModel_1.checkOpsConfig)(this.config,"read",re,e)||(t=!1)}),I&&t)return B={data:R,count:se},null!=(_=re.body)&&_.stopRes?B:void ae.send(B);ae.status(400).json({err:"Invalid Security Configuration"})}}else ae.status(400).json({err:"Invalid Security Configuration"})}catch(e){ae.status(400).json({err:e.message})}}),this.init=()=>{var e=this.lib.express.Router();return e.post("/create",(e,t)=>{this.initCreate(e,t)}),e.post("/batchCreate",(e,t)=>{this.initBatchCreate(e,t)}),e.post("/get",(e,t)=>{this.initGet(e,t)}),e.post("/update",(e,t)=>{this.initUpdate(e,t)}),e.post("/batchUpdate",(e,t)=>{this.initBatchUpdate(e,t)}),e.post("/delete",(e,t)=>{this.initDelete(e,t)}),e.post("/batchDelete",(e,t)=>{this.initBatchDelete(e,t)}),e.post("/list",(e,t)=>{this.initList(e,t)}),e},this.mongoDB=e,this.collection=t,this.isProd=i,this.config=s,this.modelChecker=r,this.lib=a;e=this.isProd?"prod":"dev";this.tableId=o?o+"-"+e:e,this.logId=null!=l?l:"",this.logWorkflow=d,this.logCollectIds=null!=n?n:[],this.debug=u}handleBuildPipeline(e){let n=[],i=new Map,u=new Set;var{query:s,sort:r,cursor:a,limit:o,stopLimit:l,pipeline:d,uniqueLabel:c,groupId:h}=e.body;try{Array.isArray(d)&&0<d.length&&d.forEach(e=>{var{condition:e,referenceSpaceLabel:s,spaceReferenceId:r,spaceReferenceAs:a,referenceSort:o,referenceFirstItem:l,referenceLimit:d}=e;if(r){let i=a||"linkedData",t=(u.add(i),{$lookup:{from:r,let:{},pipeline:[],as:i}});if(e&&0<e.length){e.forEach(e=>{t.$lookup.let["local_"+e.fromColumnId]="$"+e.fromColumnId});let i={$match:{$expr:{$and:[]}}};e.forEach(e=>{var t={};switch(e.type){case"=":t.$eq=["$"+e.toColumnId,"$$local_"+e.fromColumnId];break;case"!=":t.$ne=["$"+e.toColumnId,"$$local_"+e.fromColumnId];break;case">":t.$gt=["$"+e.toColumnId,"$$local_"+e.fromColumnId];break;case"<":t.$lt=["$"+e.toColumnId,"$$local_"+e.fromColumnId];break;case"in":t.$in=["$"+e.toColumnId,"$$local_"+e.fromColumnId];break;case"!in":t.$nin=["$"+e.toColumnId,"$$local_"+e.fromColumnId]}0<Object.keys(t).length&&i.$match.$expr.$and.push(t)}),t.$lookup.pipeline.push(i)}if(o&&o.sortId&&(a={$sort:{[o.sortId]:"asc"===o.type?1:-1}},t.$lookup.pipeline.push(a)),d&&t.$lookup.pipeline.push({$limit:d}),n.push(t),s&&0<s.length){let t={$addFields:{}};l?(n.push({$addFields:{[i]:{$first:{$ifNull:["$"+i,[]]}}}}),s.forEach(e=>{t.$addFields[i+"_"+e]=`$${i}.`+e})):s.forEach(e=>{t.$addFields[i+"_"+e]={$map:{input:"$"+i,in:"$$this."+e}}}),n.push(t)}}}),s&&0<s.length&&s.forEach(e=>{e.foreign&&Array.isArray(e.foreign)&&e.foreign.forEach(e=>{var t=e.alias;i.has(t)||(n.push({$lookup:{from:e.collection,localField:e.localField,foreignField:"_id",as:t}}),i.set(t,!0))})});let t={$sort:{}};r&&(Array.isArray(JSON.parse(JSON.stringify(r)))?r:[r]).forEach(e=>{e.sortId&&(t.$sort[e.sortId]="asc"===e.type?1:-1)});var y,p=null==r?void 0:r.sortBeforeFilter;if(p&&(0<Object.keys(t.$sort).length&&n.push(t),c)&&(n.push({$group:{_id:"$"+c,doc:{$first:"$$ROOT"}}}),n.push({$replaceRoot:{newRoot:"$doc"}})),s&&0<s.length&&(y=handleParseQueryFilter(s),n.push({$match:y})),p||(0<Object.keys(t.$sort).length&&n.push(t),c&&(n.push({$group:{_id:"$"+c,doc:{$first:"$$ROOT"}}}),n.push({$replaceRoot:{newRoot:"$doc"}}))),h&&!c&&(n.push({$group:{_id:"$"+h,topItem:{$first:"$$ROOT"},children:{$push:"$$ROOT"}}}),n.push({$replaceRoot:{newRoot:{$mergeObjects:["$topItem",{children:{$cond:{if:{$gt:[{$size:"$children"},0]},then:"$children",else:"$$REMOVE"}}}]}}}),0<Object.keys(t.$sort).length)&&n.push(t),e.body.attributeIds&&0<e.body.attributeIds.length){let t={_id:1};e.body.attributeIds.forEach(e=>{t[e]=1}),n.push({$project:t})}e.body.excludeAttributeIds&&0<e.body.excludeAttributeIds.length&&n.push({$unset:e.body.excludeAttributeIds}),a&&n.push({$skip:Number.parseInt(a)}),l||n.push({$limit:o||10});var g=Array.from(i.keys()),f=(0<g.length&&n.push({$unset:[...g]}),Array.from(u));0<f.length&&n.push({$unset:[...f]})}catch(e){}return n}}exports.MongoWrapper=MongoWrapper;
@@ -0,0 +1,120 @@
1
+ import { PubSub } from "@google-cloud/pubsub";
2
+ export interface SocketWrapperOptions {
3
+ /**
4
+ * Called for every SSE subscribe request.
5
+ * Use this to mirror your SecurityMiddleware logic — e.g. validate bm_apiToken.
6
+ * Defaults to allow-all when omitted.
7
+ */
8
+ checkTopicAuth?: (room: string, req: any) => Promise<boolean>;
9
+ /** Seconds before an idle temporary subscription auto-expires. Defaults to 3 600. */
10
+ subscriptionTtlSeconds?: number;
11
+ }
12
+ /**
13
+ * @Wrapper
14
+ * SocketWrapper — Pub/Sub publisher + SSE handler factory.
15
+ *
16
+ * ### Setup (in a Next.js API helper or Express server file)
17
+ * ```ts
18
+ * import { PubSub } from "@google-cloud/pubsub";
19
+ * import { SocketWrapper } from "blixify-server/dist/apis";
20
+ *
21
+ * // INFO: null when PUBSUB_LIVE_TOPIC is not set → local dev no-op (mirrors Verdant getPubSubClient)
22
+ * const pubsub = process.env.PUBSUB_LIVE_TOPIC ? new PubSub() : null;
23
+ *
24
+ * export const socketWrapper = new SocketWrapper(
25
+ * pubsub,
26
+ * process.env.PUBSUB_LIVE_TOPIC ?? "",
27
+ * ["robots", "vehicles"], // INFO: opt-in whitelist — only these collections are ever emitted
28
+ * {
29
+ * checkTopicAuth: async (room, req) =>
30
+ * req.query?.bm_apiToken === process.env.SECURITY_TOKEN ||
31
+ * req.body?.bm_apiToken === process.env.SECURITY_TOKEN,
32
+ * },
33
+ * );
34
+ * ```
35
+ *
36
+ * ### Wire to MongoWrapper
37
+ * ```ts
38
+ * wrapper.afterWrite = (type, id, payload) =>
39
+ * socketWrapper.emit("robots", id, type, payload);
40
+ * ```
41
+ *
42
+ * ### SSE endpoint (Next.js)
43
+ * ```ts
44
+ * // pages/api/live/[room].ts
45
+ * export const config = { api: { bodyParser: false } };
46
+ * export default socketWrapper.createSSEHandler();
47
+ * ```
48
+ *
49
+ * ### DTW usage
50
+ * ```tsx
51
+ * // List view — room "robots"
52
+ * <DataTemplateWrapper collectionId="robots" type="list"
53
+ * bareSettings={{ bareSocketName: "robots" }} workspace={workspaceDev} />
54
+ *
55
+ * // Read/update view — room "robots-{id}"
56
+ * <DataTemplateWrapper collectionId="robots" type="read" id={robotId}
57
+ * bareSettings={{ bareSocketName: `robots-${robotId}` }} workspace={workspaceDev} />
58
+ * ```
59
+ * workspaceDev must include sseEndpoint pointing at the /api/live base.
60
+ */
61
+ export declare class SocketWrapper {
62
+ private readonly pubsub;
63
+ private readonly topicName;
64
+ private readonly liveCollections;
65
+ private readonly checkTopicAuth;
66
+ private readonly subscriptionTtlSeconds;
67
+ private readonly snapshotProviders;
68
+ private resolveCollection;
69
+ constructor(pubsub: PubSub | null, topicName: string, liveCollections: string[], options?: SocketWrapperOptions);
70
+ /**
71
+ * Publish a live-stream envelope to all SSE subscribers of a collection.
72
+ *
73
+ * Publishes to TWO Pub/Sub rooms simultaneously (mirrors socket.io dual-room emit):
74
+ * "{collection}" → received by list-view SSE clients
75
+ * "{collection}-{id}" → received by read/update-view SSE clients (skipped when id is empty)
76
+ *
77
+ * Silent no-op when:
78
+ * - collection not in the whitelist
79
+ * - pubsub is null (local dev)
80
+ */
81
+ emit: <T>(collection: string, id: string, type: "insert" | "update" | "delete" | "snapshot", payload: T | T[]) => Promise<void>;
82
+ /**
83
+ * Register a snapshot provider for a collection.
84
+ * Called once per new SSE connection so the client can reconcile rows that
85
+ * changed while it was offline (upsert by _id).
86
+ *
87
+ * The optional `docId` parameter is passed when the client subscribes to a
88
+ * doc-level room (e.g. "robots-r001"), letting the provider query only that
89
+ * document instead of fetching the entire collection.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * socketWrapper.registerSnapshotProvider("robots", async (docId) => {
94
+ * if (docId) {
95
+ * const doc = await col.findOne({ _id: docId });
96
+ * return doc ? [doc] : [];
97
+ * }
98
+ * return col.find({}).toArray();
99
+ * });
100
+ * ```
101
+ */
102
+ registerSnapshotProvider: (collection: string, provider: (docId?: string) => Promise<any[]>) => void;
103
+ /**
104
+ * Returns an Express / Next.js request handler that streams Pub/Sub messages
105
+ * for one room as text/event-stream (SSE).
106
+ *
107
+ * Mount at:
108
+ * Next.js → pages/api/live/[room].ts
109
+ * Express → app.get("/api/live/:room", socketWrapper.createSSEHandler())
110
+ *
111
+ * The handler:
112
+ * 1. Validates auth via checkTopicAuth
113
+ * 2. Sends a snapshot envelope immediately on connect (if a provider is registered)
114
+ * 3. Creates a temporary pull subscription filtered by attributes.room = "{room}"
115
+ * 4. Streams incoming envelopes as `data: <json>\n\n`
116
+ * 5. Deletes the subscription when the client disconnects
117
+ */
118
+ createSSEHandler: () => (req: any, res: any) => Promise<void>;
119
+ }
120
+ //# sourceMappingURL=socketWrapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socketWrapper.d.ts","sourceRoot":"","sources":["../../src/apis/socketWrapper.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAG9C,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9D,qFAAqF;IACrF,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAc;IAC9C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA+C;IAC9E,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAS;IAChD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAG9B;IAOJ,OAAO,CAAC,iBAAiB,CAWvB;gBAGA,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,EAAE,EACzB,OAAO,CAAC,EAAE,oBAAoB;IAShC;;;;;;;;;;OAUG;IACH,IAAI,GAAU,CAAC,EACb,YAAY,MAAM,EAClB,IAAI,MAAM,EACV,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,EACjD,SAAS,CAAC,GAAG,CAAC,EAAE,KACf,OAAO,CAAC,IAAI,CAAC,CAiCd;IAEF;;;;;;;;;;;;;;;;;;;OAmBG;IACH,wBAAwB,GACtB,YAAY,MAAM,EAClB,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,KAC3C,IAAI,CAEL;IAEF;;;;;;;;;;;;;;OAcG;IACH,gBAAgB,SAEP,KAAK,GAAG,EAAE,KAAK,GAAG,KAAG,OAAO,CAAC,IAAI,CAAC,CAiIvC;CACL"}
@@ -0,0 +1,7 @@
1
+ var __awaiter=this&&this.__awaiter||function(e,n,a,l){return new(a=a||Promise)(function(i,t){function o(e){try{s(l.next(e))}catch(e){t(e)}}function r(e){try{s(l.throw(e))}catch(e){t(e)}}function s(e){var t;e.done?i(e.value):((t=e.value)instanceof a?t:new a(function(e){e(t)})).then(o,r)}s((l=l.apply(e,n||[])).next())})};Object.defineProperty(exports,"__esModule",{value:!0}),exports.SocketWrapper=void 0;let uuid_1=require("uuid");class SocketWrapper{constructor(e,t,i,o){this.snapshotProviders=new Map,this.resolveCollection=t=>{if(!this.liveCollections.has(t)){let e=t;for(;;){var i=e.lastIndexOf("-");if(-1===i)break;if(e=e.slice(0,i),this.liveCollections.has(e))return e}}return t},this.emit=(e,o,r,s)=>__awaiter(this,void 0,void 0,function*(){var t=this.resolveCollection(e);if(this.liveCollections.has(t)&&this.pubsub){let e={topic:t,type:r,payload:s,ts:Date.now()};var i=t=>__awaiter(this,void 0,void 0,function*(){try{yield this.pubsub.topic(this.topicName).publishMessage({json:e,attributes:{room:t}}),console.info(`[SocketWrapper] emit room="${t}" type="${r}"`)}catch(e){console.error(`[SocketWrapper] Publish failed room="${t}"`,e)}});yield i(t),o&&(yield i(t+"-"+o))}}),this.registerSnapshotProvider=(e,t)=>{this.snapshotProviders.set(e,t)},this.createSSEHandler=()=>(c,u)=>__awaiter(this,void 0,void 0,function*(){let e,t,i,o,r=null!=(o=null!=(t=null==(e=c.params)?void 0:e.room)?t:null==(i=c.query)?void 0:i.room)?o:"";if(/^[a-zA-Z0-9_-]+$/.test(r)){var s=this.resolveCollection(r);if(r&&this.liveCollections.has(s))if(yield this.checkTopicAuth(r,c)){u.setHeader("Content-Type","text/event-stream"),u.setHeader("Cache-Control","no-cache, no-transform"),u.setHeader("Connection","keep-alive"),u.setHeader("X-Accel-Buffering","no"),"function"==typeof u.flushHeaders&&u.flushHeaders();var n=this.snapshotProviders.get(s);if(n)try{var a={topic:s,type:"snapshot",payload:yield n(r!==s?r.slice(s.length+1):void 0),ts:Date.now()};u.write(`id: ${a.ts}
2
+ data: ${JSON.stringify(a)}
3
+
4
+ `)}catch(e){}if(this.pubsub){n=`dtw-${r.slice(0,200)}-`+(0,uuid_1.v4)();let e=null;try{var l=this.pubsub.topic(this.topicName);[e]=yield l.createSubscription(n,{messageRetentionDuration:{seconds:600},expirationPolicy:{ttl:{seconds:this.subscriptionTtlSeconds}},filter:`attributes.room = "${r}"`})}catch(e){return console.error(`[SocketWrapper] Failed to create subscription room="${r}"`,e),void u.status(500).end()}let t=t=>{try{var e=t.data.toString(),i=JSON.parse(e);u.write(`id: ${i.ts}
5
+ data: ${e}
6
+
7
+ `),t.ack()}catch(e){t.nack()}};c.on("close",()=>__awaiter(this,void 0,void 0,function*(){null!=e&&e.removeListener("message",t);try{yield null==e?void 0:e.delete()}catch(e){}u.end()})),e.on("message",t),e.on("error",e=>{console.error(`[SocketWrapper] Subscription error room="${r}"`,e)})}else{let t=setInterval(()=>{try{u.write(": ping\n\n")}catch(e){clearInterval(t)}},25e3);void c.on("close",()=>{clearInterval(t),u.end()})}}else u.status(401).json({err:"Unauthorized"});else u.status(400).json({err:`Room "${r}" is not available for live updates`})}else u.status(400).json({err:"Invalid room name"})}),this.pubsub=e,this.topicName=t,this.liveCollections=new Set(i),this.checkTopicAuth=null!=(e=null==o?void 0:o.checkTopicAuth)?e:()=>__awaiter(this,void 0,void 0,function*(){return!0}),this.subscriptionTtlSeconds=null!=(t=null==o?void 0:o.subscriptionTtlSeconds)?t:3600}}exports.SocketWrapper=SocketWrapper;
@@ -1 +1 @@
1
- {"root":["../src/apis/authWrapper.ts","../src/apis/crypto.ts","../src/apis/dayjs.ts","../src/apis/fbWrapper.ts","../src/apis/googleAnalyticsWrapper.ts","../src/apis/index.ts","../src/apis/mondayWrapper.ts","../src/apis/mongoWrapper.ts","../src/apis/postgresqlWrapper.ts","../src/apis/security.ts","../src/apis/trackVisionWrapper.ts","../src/apis/uploadWrapper.ts","../src/apis/utils.ts"],"version":"5.9.3"}
1
+ {"root":["../src/apis/authWrapper.ts","../src/apis/crypto.ts","../src/apis/dayjs.ts","../src/apis/fbWrapper.ts","../src/apis/googleAnalyticsWrapper.ts","../src/apis/index.ts","../src/apis/mondayWrapper.ts","../src/apis/mongoWrapper.ts","../src/apis/postgresqlWrapper.ts","../src/apis/security.ts","../src/apis/socketWrapper.ts","../src/apis/trackVisionWrapper.ts","../src/apis/uploadWrapper.ts","../src/apis/utils.ts"],"version":"5.9.3"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * SocketEnvelope — shared wire type for live-stream messages.
3
+ * Mirrored in blixify-ui-web src/components/data/dataTemplate/model.tsx
4
+ */
5
+ export interface SocketEnvelope<T = any> {
6
+ /** Collection name (equals the socket.io room base, e.g. "robots"). */
7
+ topic: string;
8
+ /**
9
+ * insert — a new document was created
10
+ * update — an existing document was modified
11
+ * delete — a document was removed
12
+ * snapshot — full page snapshot sent on (re)connect for offline reconciliation
13
+ */
14
+ type: "insert" | "update" | "delete" | "snapshot";
15
+ /**
16
+ * insert/update: single document T
17
+ * delete: deleted document (at minimum { _id: string })
18
+ * snapshot: T[]
19
+ */
20
+ payload: T | T[];
21
+ /** Unix timestamp (ms) set on the server. */
22
+ ts: number;
23
+ }
24
+ /**
25
+ * Room name for list-level subscriptions.
26
+ * DTW list views subscribe here to receive insert/update/delete for the whole collection.
27
+ * @example buildListRoom("robots") → "robots"
28
+ */
29
+ export declare const buildListRoom: (collection: string) => string;
30
+ /**
31
+ * Room name for document-level subscriptions.
32
+ * DTW read/update views subscribe here to receive updates for a single document.
33
+ * @example buildDocRoom("robots", "r001") → "robots-r001"
34
+ */
35
+ export declare const buildDocRoom: (collection: string, id: string) => string;
36
+ //# sourceMappingURL=socket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket.d.ts","sourceRoot":"","sources":["../../src/model/socket.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,cAAc,CAAC,CAAC,GAAG,GAAG;IACrC,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;IAClD;;;;OAIG;IACH,OAAO,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;IACjB,6CAA6C;IAC7C,EAAE,EAAE,MAAM,CAAC;CACZ;AAID;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,YAAY,MAAM,KAAG,MAAoB,CAAC;AAExE;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,YAAY,MAAM,EAAE,IAAI,MAAM,KAAG,MACvC,CAAC"}
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,"__esModule",{value:!0}),exports.buildDocRoom=exports.buildListRoom=void 0;let buildListRoom=o=>o,buildDocRoom=(exports.buildListRoom=buildListRoom,(o,i)=>o+"-"+i);exports.buildDocRoom=buildDocRoom;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blixify-server",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "license": "MIT",
5
5
  "main": "dist/apis/index.js",
6
6
  "private": false,
@@ -46,17 +46,19 @@
46
46
  "dependencies": {
47
47
  "@google-analytics/data": "^5.1.0",
48
48
  "@google-cloud/bigquery": "^8.1.0",
49
+ "@google-cloud/pubsub": "^5.3.0",
49
50
  "axios": "^1.4.0",
50
51
  "crypto-js": "^4.2.0",
52
+ "dayjs": "^1.11.13",
51
53
  "mime": "^3.0.0",
52
54
  "pg": "^8.12.0",
53
- "uuid": "^9.0.1",
54
- "dayjs": "^1.11.13"
55
+ "uuid": "^9.0.1"
55
56
  },
56
57
  "devDependencies": {
57
58
  "@types/express": "^4.17.14",
58
59
  "@types/jest": "^30.0.0",
59
60
  "@types/node": "^20.0.0",
61
+ "@types/uuid": "^9.0.7",
60
62
  "@typescript-eslint/eslint-plugin": "^7.0.0",
61
63
  "@typescript-eslint/parser": "^7.0.0",
62
64
  "aws-sdk": "^2.1432.0",
@@ -78,7 +80,6 @@
78
80
  "multer": "^1.4.5-lts.1",
79
81
  "ts-jest": "^29.4.9",
80
82
  "ts-node": "^10.9.2",
81
- "typescript": "^5.5.4",
82
- "@types/uuid": "^9.0.7"
83
+ "typescript": "^5.5.4"
83
84
  }
84
85
  }