blixify-server 1.0.1 → 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 +159 -0
- package/dist/apis/index.d.ts +4 -0
- package/dist/apis/index.d.ts.map +1 -1
- package/dist/apis/index.js +1 -1
- package/dist/apis/mondayWrapper.d.ts.map +1 -1
- package/dist/apis/mongoWrapper.d.ts +8 -0
- package/dist/apis/mongoWrapper.d.ts.map +1 -1
- package/dist/apis/mongoWrapper.js +1 -1
- package/dist/apis/socketWrapper.d.ts +120 -0
- package/dist/apis/socketWrapper.d.ts.map +1 -0
- package/dist/apis/socketWrapper.js +7 -0
- package/dist/buildtsconfig.tsbuildinfo +1 -1
- package/dist/model/socket.d.ts +36 -0
- package/dist/model/socket.d.ts.map +1 -0
- package/dist/model/socket.js +1 -0
- package/package.json +6 -5
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.
|
package/dist/apis/index.d.ts
CHANGED
|
@@ -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
|
package/dist/apis/index.d.ts.map
CHANGED
|
@@ -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"}
|
package/dist/apis/index.js
CHANGED
|
@@ -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")),
|
|
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;;
|
|
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"}
|
|
@@ -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;
|
|
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.
|
|
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
|
}
|