@plasius/graph-runtime-azure-functions 0.1.1 → 0.1.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 +107 -6
- package/dist/index.cjs +176 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -2
- package/dist/index.d.ts +15 -2
- package/dist/index.js +176 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,22 +1,123 @@
|
|
|
1
1
|
# @plasius/graph-runtime-azure-functions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@plasius/graph-runtime-azure-functions)
|
|
4
|
+
[](https://github.com/Plasius-LTD/graph-runtime-azure-functions/actions/workflows/ci.yml)
|
|
5
|
+
[](https://codecov.io/gh/Plasius-LTD/graph-runtime-azure-functions)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](./CODE_OF_CONDUCT.md)
|
|
8
|
+
[](./SECURITY.md)
|
|
9
|
+
[](./CHANGELOG.md)
|
|
10
|
+
|
|
11
|
+
[](https://github.com/Plasius-LTD/graph-runtime-azure-functions/actions/workflows/ci.yml)
|
|
12
|
+
[](https://github.com/Plasius-LTD/graph-runtime-azure-functions/actions/workflows/cd.yml)
|
|
13
|
+
|
|
14
|
+
Azure Functions runtime adapter for graph read and write HTTP handlers.
|
|
15
|
+
|
|
16
|
+
Apache-2.0. ESM + CJS builds. TypeScript types included.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Node.js 24+ (matches `.nvmrc` and CI/CD)
|
|
23
|
+
- `@azure/functions` 4.x (`peerDependencies`)
|
|
24
|
+
- `@plasius/graph-gateway-core`
|
|
25
|
+
- `@plasius/graph-write-coordinator`
|
|
26
|
+
|
|
27
|
+
---
|
|
4
28
|
|
|
5
29
|
## Installation
|
|
6
30
|
|
|
7
|
-
|
|
31
|
+
```bash
|
|
32
|
+
npm install @plasius/graph-runtime-azure-functions @azure/functions
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Exports
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import {
|
|
41
|
+
createGraphReadHandler,
|
|
42
|
+
createGraphWriteHandler,
|
|
43
|
+
type GraphReadHandlerOptions,
|
|
44
|
+
type GraphWriteHandlerOptions,
|
|
45
|
+
} from "@plasius/graph-runtime-azure-functions";
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { app } from "@azure/functions";
|
|
54
|
+
import {
|
|
55
|
+
createGraphReadHandler,
|
|
56
|
+
createGraphWriteHandler,
|
|
57
|
+
} from "@plasius/graph-runtime-azure-functions";
|
|
58
|
+
|
|
59
|
+
app.http("graph-read", {
|
|
60
|
+
methods: ["POST"],
|
|
61
|
+
authLevel: "function",
|
|
62
|
+
handler: createGraphReadHandler({ gateway, telemetry }),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
app.http("graph-write", {
|
|
66
|
+
methods: ["POST"],
|
|
67
|
+
authLevel: "function",
|
|
68
|
+
handler: createGraphWriteHandler({ coordinator, telemetry }),
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Security and Input Validation
|
|
75
|
+
|
|
76
|
+
Handler factories enforce boundary validation for external input:
|
|
77
|
+
|
|
78
|
+
- Read payloads must satisfy `isGraphQuery`.
|
|
79
|
+
- Write payloads must satisfy strict write-command shape rules (idempotency/partition/aggregate keys + payload + timestamp).
|
|
80
|
+
- Optional payload size guard via `maxBodyBytes` (default `64KB`, returns `413` when exceeded).
|
|
81
|
+
- Optional authorization guard via `authorize(context)` (returns `403` when denied).
|
|
82
|
+
|
|
83
|
+
Failure responses are bounded and sanitized:
|
|
84
|
+
|
|
85
|
+
- Read validation/auth/body-limit: `400/403/413`
|
|
86
|
+
- Read upstream execution failure: `502`
|
|
87
|
+
- Write validation/auth/body-limit: `400/403/413`
|
|
88
|
+
- Write upstream execution failure: `503`
|
|
89
|
+
|
|
90
|
+
---
|
|
8
91
|
|
|
9
92
|
## Development
|
|
10
93
|
|
|
94
|
+
```bash
|
|
95
|
+
npm run clean
|
|
11
96
|
npm install
|
|
97
|
+
npm run lint
|
|
98
|
+
npm run typecheck
|
|
99
|
+
npm run test:coverage
|
|
12
100
|
npm run build
|
|
13
|
-
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Telemetry
|
|
106
|
+
|
|
107
|
+
Both handlers accept optional `telemetry` (`TelemetrySink`) and emit:
|
|
108
|
+
|
|
109
|
+
- Read: `graph.runtime.read.request`, `graph.runtime.read.latency`, `graph.runtime.read.error`
|
|
110
|
+
- Write: `graph.runtime.write.request`, `graph.runtime.write.latency`, `graph.runtime.write.error`
|
|
111
|
+
|
|
112
|
+
---
|
|
14
113
|
|
|
15
114
|
## Architecture
|
|
16
115
|
|
|
17
|
-
-
|
|
18
|
-
- Cross-package
|
|
116
|
+
- Package ADRs: [`docs/adrs`](./docs/adrs)
|
|
117
|
+
- Cross-package ADRs: `plasius-ltd-site/docs/adrs/adr-0020` to `adr-0024`
|
|
118
|
+
|
|
119
|
+
---
|
|
19
120
|
|
|
20
121
|
## License
|
|
21
122
|
|
|
22
|
-
Apache-2.0
|
|
123
|
+
Licensed under the [Apache-2.0 License](./LICENSE).
|
package/dist/index.cjs
CHANGED
|
@@ -26,6 +26,40 @@ __export(index_exports, {
|
|
|
26
26
|
module.exports = __toCommonJS(index_exports);
|
|
27
27
|
|
|
28
28
|
// src/handlers.ts
|
|
29
|
+
var import_graph_contracts = require("@plasius/graph-contracts");
|
|
30
|
+
var DEFAULT_MAX_BODY_BYTES = 64 * 1024;
|
|
31
|
+
var isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
32
|
+
var isSafeKey = (value) => /^[A-Za-z0-9:_-]+$/.test(value);
|
|
33
|
+
var isWriteCommandPayloadValid = (value) => {
|
|
34
|
+
if (!isObject(value)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (typeof value.idempotencyKey !== "string" || value.idempotencyKey.length === 0 || value.idempotencyKey.length > 128 || !isSafeKey(value.idempotencyKey)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (typeof value.partitionKey !== "string" || value.partitionKey.length === 0 || value.partitionKey.length > 128 || !isSafeKey(value.partitionKey)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (typeof value.aggregateKey !== "string" || value.aggregateKey.length === 0 || value.aggregateKey.length > 256 || !isSafeKey(value.aggregateKey)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (!isObject(value.payload)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (typeof value.submittedAtEpochMs !== "number" || !Number.isFinite(value.submittedAtEpochMs) || value.submittedAtEpochMs < 0) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return value.actorId === void 0 || typeof value.actorId === "string" && value.actorId.length > 0 && value.actorId.length <= 128;
|
|
53
|
+
};
|
|
54
|
+
var HandlerValidationError = class extends Error {
|
|
55
|
+
status;
|
|
56
|
+
code;
|
|
57
|
+
constructor(status, code, message) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.status = status;
|
|
60
|
+
this.code = code;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
29
63
|
var jsonResponse = (status, body) => ({
|
|
30
64
|
status,
|
|
31
65
|
jsonBody: body,
|
|
@@ -34,34 +68,167 @@ var jsonResponse = (status, body) => ({
|
|
|
34
68
|
"cache-control": "no-store"
|
|
35
69
|
}
|
|
36
70
|
});
|
|
71
|
+
var getContentLength = (request) => {
|
|
72
|
+
const raw = request.headers?.get?.("content-length");
|
|
73
|
+
if (!raw) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const parsed = Number.parseInt(raw, 10);
|
|
77
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return parsed;
|
|
81
|
+
};
|
|
82
|
+
var enforceBodyLimit = (request, maxBodyBytes) => {
|
|
83
|
+
const contentLength = getContentLength(request);
|
|
84
|
+
if (contentLength !== null && contentLength > maxBodyBytes) {
|
|
85
|
+
throw new HandlerValidationError(413, "GRAPH_REQUEST_BODY_TOO_LARGE", "Request payload exceeds allowed limit.");
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
var enforceAuthorization = async (authorize, operation, request, context) => {
|
|
89
|
+
if (!authorize) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const allowed = await authorize({ operation, request, context });
|
|
93
|
+
if (!allowed) {
|
|
94
|
+
throw new HandlerValidationError(403, "GRAPH_FORBIDDEN", "Forbidden");
|
|
95
|
+
}
|
|
96
|
+
};
|
|
37
97
|
var createGraphReadHandler = (options) => {
|
|
38
98
|
return async (request, context) => {
|
|
99
|
+
const startedAt = Date.now();
|
|
100
|
+
options.telemetry?.metric({
|
|
101
|
+
name: "graph.runtime.read.request",
|
|
102
|
+
value: 1,
|
|
103
|
+
unit: "count"
|
|
104
|
+
});
|
|
39
105
|
try {
|
|
40
|
-
const
|
|
106
|
+
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
107
|
+
enforceBodyLimit(request, maxBodyBytes);
|
|
108
|
+
await enforceAuthorization(options.authorize, "read", request, context);
|
|
109
|
+
let rawBody;
|
|
110
|
+
try {
|
|
111
|
+
rawBody = await request.json();
|
|
112
|
+
} catch {
|
|
113
|
+
throw new HandlerValidationError(400, "GRAPH_READ_REQUEST_INVALID", "Invalid graph read request.");
|
|
114
|
+
}
|
|
115
|
+
if (!(0, import_graph_contracts.isGraphQuery)(rawBody)) {
|
|
116
|
+
throw new HandlerValidationError(400, "GRAPH_READ_REQUEST_INVALID", "Invalid graph read request.");
|
|
117
|
+
}
|
|
118
|
+
const query = rawBody;
|
|
41
119
|
const result = await options.gateway.execute({
|
|
42
120
|
...query,
|
|
43
121
|
traceId: query.traceId ?? context.traceContext?.traceParent
|
|
44
122
|
});
|
|
123
|
+
options.telemetry?.metric({
|
|
124
|
+
name: "graph.runtime.read.latency",
|
|
125
|
+
value: Date.now() - startedAt,
|
|
126
|
+
unit: "ms",
|
|
127
|
+
tags: { status: "200" }
|
|
128
|
+
});
|
|
45
129
|
return jsonResponse(200, result);
|
|
46
130
|
} catch (error) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
131
|
+
if (error instanceof HandlerValidationError) {
|
|
132
|
+
options.telemetry?.metric({
|
|
133
|
+
name: "graph.runtime.read.error",
|
|
134
|
+
value: 1,
|
|
135
|
+
unit: "count",
|
|
136
|
+
tags: { code: error.code }
|
|
137
|
+
});
|
|
138
|
+
options.telemetry?.error({
|
|
139
|
+
message: error.message,
|
|
140
|
+
source: "graph-runtime-azure-functions.read",
|
|
141
|
+
code: error.code
|
|
142
|
+
});
|
|
143
|
+
return jsonResponse(error.status, {
|
|
144
|
+
code: error.code,
|
|
145
|
+
message: error.message
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
options.telemetry?.metric({
|
|
149
|
+
name: "graph.runtime.read.error",
|
|
150
|
+
value: 1,
|
|
151
|
+
unit: "count",
|
|
152
|
+
tags: { code: "GRAPH_READ_UPSTREAM_FAILED" }
|
|
153
|
+
});
|
|
154
|
+
options.telemetry?.error({
|
|
155
|
+
message: "Graph read failed",
|
|
156
|
+
source: "graph-runtime-azure-functions.read",
|
|
157
|
+
code: "GRAPH_READ_UPSTREAM_FAILED"
|
|
158
|
+
});
|
|
159
|
+
return jsonResponse(502, {
|
|
160
|
+
code: "GRAPH_READ_UPSTREAM_FAILED",
|
|
161
|
+
message: "Graph read failed."
|
|
50
162
|
});
|
|
51
163
|
}
|
|
52
164
|
};
|
|
53
165
|
};
|
|
54
166
|
var createGraphWriteHandler = (options) => {
|
|
55
|
-
return async (request) => {
|
|
167
|
+
return async (request, context) => {
|
|
168
|
+
const startedAt = Date.now();
|
|
169
|
+
options.telemetry?.metric({
|
|
170
|
+
name: "graph.runtime.write.request",
|
|
171
|
+
value: 1,
|
|
172
|
+
unit: "count"
|
|
173
|
+
});
|
|
56
174
|
try {
|
|
57
|
-
const
|
|
175
|
+
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
176
|
+
enforceBodyLimit(request, maxBodyBytes);
|
|
177
|
+
await enforceAuthorization(options.authorize, "write", request, context);
|
|
178
|
+
let rawBody;
|
|
179
|
+
try {
|
|
180
|
+
rawBody = await request.json();
|
|
181
|
+
} catch {
|
|
182
|
+
throw new HandlerValidationError(400, "GRAPH_WRITE_REQUEST_INVALID", "Invalid graph write request.");
|
|
183
|
+
}
|
|
184
|
+
if (!isWriteCommandPayloadValid(rawBody)) {
|
|
185
|
+
throw new HandlerValidationError(400, "GRAPH_WRITE_REQUEST_INVALID", "Invalid graph write request.");
|
|
186
|
+
}
|
|
187
|
+
const command = rawBody;
|
|
58
188
|
const operation = await options.coordinator.submit(command);
|
|
59
189
|
const status = operation.state === "succeeded" ? 200 : 202;
|
|
190
|
+
options.telemetry?.metric({
|
|
191
|
+
name: "graph.runtime.write.latency",
|
|
192
|
+
value: Date.now() - startedAt,
|
|
193
|
+
unit: "ms",
|
|
194
|
+
tags: {
|
|
195
|
+
status: String(status),
|
|
196
|
+
state: operation.state
|
|
197
|
+
}
|
|
198
|
+
});
|
|
60
199
|
return jsonResponse(status, operation);
|
|
61
200
|
} catch (error) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
201
|
+
if (error instanceof HandlerValidationError) {
|
|
202
|
+
options.telemetry?.metric({
|
|
203
|
+
name: "graph.runtime.write.error",
|
|
204
|
+
value: 1,
|
|
205
|
+
unit: "count",
|
|
206
|
+
tags: { code: error.code }
|
|
207
|
+
});
|
|
208
|
+
options.telemetry?.error({
|
|
209
|
+
message: error.message,
|
|
210
|
+
source: "graph-runtime-azure-functions.write",
|
|
211
|
+
code: error.code
|
|
212
|
+
});
|
|
213
|
+
return jsonResponse(error.status, {
|
|
214
|
+
code: error.code,
|
|
215
|
+
message: error.message
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
options.telemetry?.metric({
|
|
219
|
+
name: "graph.runtime.write.error",
|
|
220
|
+
value: 1,
|
|
221
|
+
unit: "count",
|
|
222
|
+
tags: { code: "GRAPH_WRITE_UPSTREAM_FAILED" }
|
|
223
|
+
});
|
|
224
|
+
options.telemetry?.error({
|
|
225
|
+
message: "Graph write failed",
|
|
226
|
+
source: "graph-runtime-azure-functions.write",
|
|
227
|
+
code: "GRAPH_WRITE_UPSTREAM_FAILED"
|
|
228
|
+
});
|
|
229
|
+
return jsonResponse(503, {
|
|
230
|
+
code: "GRAPH_WRITE_UPSTREAM_FAILED",
|
|
231
|
+
message: "Graph write failed."
|
|
65
232
|
});
|
|
66
233
|
}
|
|
67
234
|
};
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/handlers.ts"],"sourcesContent":["export * from \"./handlers.js\";\n","import type { HttpHandler, HttpRequest, HttpResponseInit, InvocationContext } from \"@azure/functions\";\nimport type { GraphQuery, WriteCommand } from \"@plasius/graph-contracts\";\nimport type { GraphGateway } from \"@plasius/graph-gateway-core\";\nimport type { WriteCoordinator } from \"@plasius/graph-write-coordinator\";\n\nexport interface GraphReadHandlerOptions {\n gateway: Pick<GraphGateway, \"execute\">;\n}\n\nexport interface GraphWriteHandlerOptions {\n coordinator: Pick<WriteCoordinator, \"submit\">;\n}\n\nconst jsonResponse = (status: number, body: unknown): HttpResponseInit => ({\n status,\n jsonBody: body,\n headers: {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n },\n});\n\nexport const createGraphReadHandler = (options: GraphReadHandlerOptions): HttpHandler => {\n return async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {\n try {\n const query = (await request.json()) as GraphQuery;\n const result = await options.gateway.execute({\n ...query,\n traceId: query.traceId ?? context.traceContext?.traceParent,\n });\n return jsonResponse(200, result);\n } catch (error) {\n return jsonResponse(400, {\n code: \"GRAPH_READ_REQUEST_INVALID\",\n message: error instanceof Error ? error.message : \"Invalid read request\",\n });\n }\n };\n};\n\nexport const createGraphWriteHandler = (options: GraphWriteHandlerOptions): HttpHandler => {\n return async (request: HttpRequest): Promise<HttpResponseInit> => {\n try {\n const command = (await request.json()) as WriteCommand;\n const operation = await options.coordinator.submit(command);\n const status = operation.state === \"succeeded\" ? 200 : 202;\n return jsonResponse(status, operation);\n } catch (error) {\n return jsonResponse(400, {\n code: \"GRAPH_WRITE_REQUEST_INVALID\",\n message: error instanceof Error ? error.message : \"Invalid write request\",\n });\n }\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACaA,IAAM,eAAe,CAAC,QAAgB,UAAqC;AAAA,EACzE;AAAA,EACA,UAAU;AAAA,EACV,SAAS;AAAA,IACP,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,EACnB;AACF;AAEO,IAAM,yBAAyB,CAAC,YAAkD;AACvF,SAAO,OAAO,SAAsB,YAA0D;AAC5F,QAAI;AACF,YAAM,QAAS,MAAM,QAAQ,KAAK;AAClC,YAAM,SAAS,MAAM,QAAQ,QAAQ,QAAQ;AAAA,QAC3C,GAAG;AAAA,QACH,SAAS,MAAM,WAAW,QAAQ,cAAc;AAAA,MAClD,CAAC;AACD,aAAO,aAAa,KAAK,MAAM;AAAA,IACjC,SAAS,OAAO;AACd,aAAO,aAAa,KAAK;AAAA,QACvB,MAAM;AAAA,QACN,SAAS,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MACpD,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAEO,IAAM,0BAA0B,CAAC,YAAmD;AACzF,SAAO,OAAO,YAAoD;AAChE,QAAI;AACF,YAAM,UAAW,MAAM,QAAQ,KAAK;AACpC,YAAM,YAAY,MAAM,QAAQ,YAAY,OAAO,OAAO;AAC1D,YAAM,SAAS,UAAU,UAAU,cAAc,MAAM;AACvD,aAAO,aAAa,QAAQ,SAAS;AAAA,IACvC,SAAS,OAAO;AACd,aAAO,aAAa,KAAK;AAAA,QACvB,MAAM;AAAA,QACN,SAAS,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MACpD,CAAC;AAAA,IACH;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/handlers.ts"],"sourcesContent":["export * from \"./handlers.js\";\n","import type { HttpHandler, HttpRequest, HttpResponseInit, InvocationContext } from \"@azure/functions\";\nimport { isGraphQuery, type GraphQuery, type TelemetrySink, type WriteCommand } from \"@plasius/graph-contracts\";\nimport type { GraphGateway } from \"@plasius/graph-gateway-core\";\nimport type { WriteCoordinator } from \"@plasius/graph-write-coordinator\";\n\nexport interface HandlerAuthContext {\n operation: \"read\" | \"write\";\n request: HttpRequest;\n context: InvocationContext;\n}\n\nexport type AuthorizeHandler = (context: HandlerAuthContext) => Promise<boolean> | boolean;\n\nexport interface GraphReadHandlerOptions {\n gateway: Pick<GraphGateway, \"execute\">;\n telemetry?: TelemetrySink;\n authorize?: AuthorizeHandler;\n maxBodyBytes?: number;\n}\n\nexport interface GraphWriteHandlerOptions {\n coordinator: Pick<WriteCoordinator, \"submit\">;\n telemetry?: TelemetrySink;\n authorize?: AuthorizeHandler;\n maxBodyBytes?: number;\n}\n\nconst DEFAULT_MAX_BODY_BYTES = 64 * 1024;\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n value !== null && typeof value === \"object\" && !Array.isArray(value);\n\nconst isSafeKey = (value: string): boolean => /^[A-Za-z0-9:_-]+$/.test(value);\n\nconst isWriteCommandPayloadValid = (value: unknown): value is WriteCommand => {\n if (!isObject(value)) {\n return false;\n }\n\n if (\n typeof value.idempotencyKey !== \"string\"\n || value.idempotencyKey.length === 0\n || value.idempotencyKey.length > 128\n || !isSafeKey(value.idempotencyKey)\n ) {\n return false;\n }\n\n if (\n typeof value.partitionKey !== \"string\"\n || value.partitionKey.length === 0\n || value.partitionKey.length > 128\n || !isSafeKey(value.partitionKey)\n ) {\n return false;\n }\n\n if (\n typeof value.aggregateKey !== \"string\"\n || value.aggregateKey.length === 0\n || value.aggregateKey.length > 256\n || !isSafeKey(value.aggregateKey)\n ) {\n return false;\n }\n\n if (!isObject(value.payload)) {\n return false;\n }\n\n if (typeof value.submittedAtEpochMs !== \"number\" || !Number.isFinite(value.submittedAtEpochMs) || value.submittedAtEpochMs < 0) {\n return false;\n }\n\n return value.actorId === undefined || (typeof value.actorId === \"string\" && value.actorId.length > 0 && value.actorId.length <= 128);\n};\n\nclass HandlerValidationError extends Error {\n public readonly status: number;\n public readonly code: string;\n\n public constructor(status: number, code: string, message: string) {\n super(message);\n this.status = status;\n this.code = code;\n }\n}\n\nconst jsonResponse = (status: number, body: unknown): HttpResponseInit => ({\n status,\n jsonBody: body,\n headers: {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n },\n});\n\nconst getContentLength = (request: HttpRequest): number | null => {\n const raw = request.headers?.get?.(\"content-length\");\n if (!raw) {\n return null;\n }\n\n const parsed = Number.parseInt(raw, 10);\n if (!Number.isFinite(parsed) || parsed < 0) {\n return null;\n }\n\n return parsed;\n};\n\nconst enforceBodyLimit = (request: HttpRequest, maxBodyBytes: number): void => {\n const contentLength = getContentLength(request);\n if (contentLength !== null && contentLength > maxBodyBytes) {\n throw new HandlerValidationError(413, \"GRAPH_REQUEST_BODY_TOO_LARGE\", \"Request payload exceeds allowed limit.\");\n }\n};\n\nconst enforceAuthorization = async (\n authorize: AuthorizeHandler | undefined,\n operation: \"read\" | \"write\",\n request: HttpRequest,\n context: InvocationContext,\n): Promise<void> => {\n if (!authorize) {\n return;\n }\n\n const allowed = await authorize({ operation, request, context });\n if (!allowed) {\n throw new HandlerValidationError(403, \"GRAPH_FORBIDDEN\", \"Forbidden\");\n }\n};\n\nexport const createGraphReadHandler = (options: GraphReadHandlerOptions): HttpHandler => {\n return async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {\n const startedAt = Date.now();\n options.telemetry?.metric({\n name: \"graph.runtime.read.request\",\n value: 1,\n unit: \"count\",\n });\n try {\n const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;\n enforceBodyLimit(request, maxBodyBytes);\n await enforceAuthorization(options.authorize, \"read\", request, context);\n\n let rawBody: unknown;\n try {\n rawBody = await request.json();\n } catch {\n throw new HandlerValidationError(400, \"GRAPH_READ_REQUEST_INVALID\", \"Invalid graph read request.\");\n }\n if (!isGraphQuery(rawBody)) {\n throw new HandlerValidationError(400, \"GRAPH_READ_REQUEST_INVALID\", \"Invalid graph read request.\");\n }\n\n const query = rawBody as GraphQuery;\n const result = await options.gateway.execute({\n ...query,\n traceId: query.traceId ?? context.traceContext?.traceParent,\n });\n options.telemetry?.metric({\n name: \"graph.runtime.read.latency\",\n value: Date.now() - startedAt,\n unit: \"ms\",\n tags: { status: \"200\" },\n });\n return jsonResponse(200, result);\n } catch (error) {\n if (error instanceof HandlerValidationError) {\n options.telemetry?.metric({\n name: \"graph.runtime.read.error\",\n value: 1,\n unit: \"count\",\n tags: { code: error.code },\n });\n options.telemetry?.error({\n message: error.message,\n source: \"graph-runtime-azure-functions.read\",\n code: error.code,\n });\n return jsonResponse(error.status, {\n code: error.code,\n message: error.message,\n });\n }\n\n options.telemetry?.metric({\n name: \"graph.runtime.read.error\",\n value: 1,\n unit: \"count\",\n tags: { code: \"GRAPH_READ_UPSTREAM_FAILED\" },\n });\n options.telemetry?.error({\n message: \"Graph read failed\",\n source: \"graph-runtime-azure-functions.read\",\n code: \"GRAPH_READ_UPSTREAM_FAILED\",\n });\n return jsonResponse(502, {\n code: \"GRAPH_READ_UPSTREAM_FAILED\",\n message: \"Graph read failed.\",\n });\n }\n };\n};\n\nexport const createGraphWriteHandler = (options: GraphWriteHandlerOptions): HttpHandler => {\n return async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {\n const startedAt = Date.now();\n options.telemetry?.metric({\n name: \"graph.runtime.write.request\",\n value: 1,\n unit: \"count\",\n });\n try {\n const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;\n enforceBodyLimit(request, maxBodyBytes);\n await enforceAuthorization(options.authorize, \"write\", request, context);\n\n let rawBody: unknown;\n try {\n rawBody = await request.json();\n } catch {\n throw new HandlerValidationError(400, \"GRAPH_WRITE_REQUEST_INVALID\", \"Invalid graph write request.\");\n }\n if (!isWriteCommandPayloadValid(rawBody)) {\n throw new HandlerValidationError(400, \"GRAPH_WRITE_REQUEST_INVALID\", \"Invalid graph write request.\");\n }\n\n const command = rawBody as WriteCommand;\n const operation = await options.coordinator.submit(command);\n const status = operation.state === \"succeeded\" ? 200 : 202;\n options.telemetry?.metric({\n name: \"graph.runtime.write.latency\",\n value: Date.now() - startedAt,\n unit: \"ms\",\n tags: {\n status: String(status),\n state: operation.state,\n },\n });\n return jsonResponse(status, operation);\n } catch (error) {\n if (error instanceof HandlerValidationError) {\n options.telemetry?.metric({\n name: \"graph.runtime.write.error\",\n value: 1,\n unit: \"count\",\n tags: { code: error.code },\n });\n options.telemetry?.error({\n message: error.message,\n source: \"graph-runtime-azure-functions.write\",\n code: error.code,\n });\n return jsonResponse(error.status, {\n code: error.code,\n message: error.message,\n });\n }\n\n options.telemetry?.metric({\n name: \"graph.runtime.write.error\",\n value: 1,\n unit: \"count\",\n tags: { code: \"GRAPH_WRITE_UPSTREAM_FAILED\" },\n });\n options.telemetry?.error({\n message: \"Graph write failed\",\n source: \"graph-runtime-azure-functions.write\",\n code: \"GRAPH_WRITE_UPSTREAM_FAILED\",\n });\n return jsonResponse(503, {\n code: \"GRAPH_WRITE_UPSTREAM_FAILED\",\n message: \"Graph write failed.\",\n });\n }\n };\n};\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
|
-
import { HttpHandler } from '@azure/functions';
|
|
1
|
+
import { HttpRequest, InvocationContext, HttpHandler } from '@azure/functions';
|
|
2
|
+
import { TelemetrySink } from '@plasius/graph-contracts';
|
|
2
3
|
import { GraphGateway } from '@plasius/graph-gateway-core';
|
|
3
4
|
import { WriteCoordinator } from '@plasius/graph-write-coordinator';
|
|
4
5
|
|
|
6
|
+
interface HandlerAuthContext {
|
|
7
|
+
operation: "read" | "write";
|
|
8
|
+
request: HttpRequest;
|
|
9
|
+
context: InvocationContext;
|
|
10
|
+
}
|
|
11
|
+
type AuthorizeHandler = (context: HandlerAuthContext) => Promise<boolean> | boolean;
|
|
5
12
|
interface GraphReadHandlerOptions {
|
|
6
13
|
gateway: Pick<GraphGateway, "execute">;
|
|
14
|
+
telemetry?: TelemetrySink;
|
|
15
|
+
authorize?: AuthorizeHandler;
|
|
16
|
+
maxBodyBytes?: number;
|
|
7
17
|
}
|
|
8
18
|
interface GraphWriteHandlerOptions {
|
|
9
19
|
coordinator: Pick<WriteCoordinator, "submit">;
|
|
20
|
+
telemetry?: TelemetrySink;
|
|
21
|
+
authorize?: AuthorizeHandler;
|
|
22
|
+
maxBodyBytes?: number;
|
|
10
23
|
}
|
|
11
24
|
declare const createGraphReadHandler: (options: GraphReadHandlerOptions) => HttpHandler;
|
|
12
25
|
declare const createGraphWriteHandler: (options: GraphWriteHandlerOptions) => HttpHandler;
|
|
13
26
|
|
|
14
|
-
export { type GraphReadHandlerOptions, type GraphWriteHandlerOptions, createGraphReadHandler, createGraphWriteHandler };
|
|
27
|
+
export { type AuthorizeHandler, type GraphReadHandlerOptions, type GraphWriteHandlerOptions, type HandlerAuthContext, createGraphReadHandler, createGraphWriteHandler };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
|
-
import { HttpHandler } from '@azure/functions';
|
|
1
|
+
import { HttpRequest, InvocationContext, HttpHandler } from '@azure/functions';
|
|
2
|
+
import { TelemetrySink } from '@plasius/graph-contracts';
|
|
2
3
|
import { GraphGateway } from '@plasius/graph-gateway-core';
|
|
3
4
|
import { WriteCoordinator } from '@plasius/graph-write-coordinator';
|
|
4
5
|
|
|
6
|
+
interface HandlerAuthContext {
|
|
7
|
+
operation: "read" | "write";
|
|
8
|
+
request: HttpRequest;
|
|
9
|
+
context: InvocationContext;
|
|
10
|
+
}
|
|
11
|
+
type AuthorizeHandler = (context: HandlerAuthContext) => Promise<boolean> | boolean;
|
|
5
12
|
interface GraphReadHandlerOptions {
|
|
6
13
|
gateway: Pick<GraphGateway, "execute">;
|
|
14
|
+
telemetry?: TelemetrySink;
|
|
15
|
+
authorize?: AuthorizeHandler;
|
|
16
|
+
maxBodyBytes?: number;
|
|
7
17
|
}
|
|
8
18
|
interface GraphWriteHandlerOptions {
|
|
9
19
|
coordinator: Pick<WriteCoordinator, "submit">;
|
|
20
|
+
telemetry?: TelemetrySink;
|
|
21
|
+
authorize?: AuthorizeHandler;
|
|
22
|
+
maxBodyBytes?: number;
|
|
10
23
|
}
|
|
11
24
|
declare const createGraphReadHandler: (options: GraphReadHandlerOptions) => HttpHandler;
|
|
12
25
|
declare const createGraphWriteHandler: (options: GraphWriteHandlerOptions) => HttpHandler;
|
|
13
26
|
|
|
14
|
-
export { type GraphReadHandlerOptions, type GraphWriteHandlerOptions, createGraphReadHandler, createGraphWriteHandler };
|
|
27
|
+
export { type AuthorizeHandler, type GraphReadHandlerOptions, type GraphWriteHandlerOptions, type HandlerAuthContext, createGraphReadHandler, createGraphWriteHandler };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,38 @@
|
|
|
1
1
|
// src/handlers.ts
|
|
2
|
+
import { isGraphQuery } from "@plasius/graph-contracts";
|
|
3
|
+
var DEFAULT_MAX_BODY_BYTES = 64 * 1024;
|
|
4
|
+
var isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5
|
+
var isSafeKey = (value) => /^[A-Za-z0-9:_-]+$/.test(value);
|
|
6
|
+
var isWriteCommandPayloadValid = (value) => {
|
|
7
|
+
if (!isObject(value)) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
if (typeof value.idempotencyKey !== "string" || value.idempotencyKey.length === 0 || value.idempotencyKey.length > 128 || !isSafeKey(value.idempotencyKey)) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (typeof value.partitionKey !== "string" || value.partitionKey.length === 0 || value.partitionKey.length > 128 || !isSafeKey(value.partitionKey)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (typeof value.aggregateKey !== "string" || value.aggregateKey.length === 0 || value.aggregateKey.length > 256 || !isSafeKey(value.aggregateKey)) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (!isObject(value.payload)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (typeof value.submittedAtEpochMs !== "number" || !Number.isFinite(value.submittedAtEpochMs) || value.submittedAtEpochMs < 0) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return value.actorId === void 0 || typeof value.actorId === "string" && value.actorId.length > 0 && value.actorId.length <= 128;
|
|
26
|
+
};
|
|
27
|
+
var HandlerValidationError = class extends Error {
|
|
28
|
+
status;
|
|
29
|
+
code;
|
|
30
|
+
constructor(status, code, message) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.status = status;
|
|
33
|
+
this.code = code;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
2
36
|
var jsonResponse = (status, body) => ({
|
|
3
37
|
status,
|
|
4
38
|
jsonBody: body,
|
|
@@ -7,34 +41,167 @@ var jsonResponse = (status, body) => ({
|
|
|
7
41
|
"cache-control": "no-store"
|
|
8
42
|
}
|
|
9
43
|
});
|
|
44
|
+
var getContentLength = (request) => {
|
|
45
|
+
const raw = request.headers?.get?.("content-length");
|
|
46
|
+
if (!raw) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const parsed = Number.parseInt(raw, 10);
|
|
50
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return parsed;
|
|
54
|
+
};
|
|
55
|
+
var enforceBodyLimit = (request, maxBodyBytes) => {
|
|
56
|
+
const contentLength = getContentLength(request);
|
|
57
|
+
if (contentLength !== null && contentLength > maxBodyBytes) {
|
|
58
|
+
throw new HandlerValidationError(413, "GRAPH_REQUEST_BODY_TOO_LARGE", "Request payload exceeds allowed limit.");
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var enforceAuthorization = async (authorize, operation, request, context) => {
|
|
62
|
+
if (!authorize) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const allowed = await authorize({ operation, request, context });
|
|
66
|
+
if (!allowed) {
|
|
67
|
+
throw new HandlerValidationError(403, "GRAPH_FORBIDDEN", "Forbidden");
|
|
68
|
+
}
|
|
69
|
+
};
|
|
10
70
|
var createGraphReadHandler = (options) => {
|
|
11
71
|
return async (request, context) => {
|
|
72
|
+
const startedAt = Date.now();
|
|
73
|
+
options.telemetry?.metric({
|
|
74
|
+
name: "graph.runtime.read.request",
|
|
75
|
+
value: 1,
|
|
76
|
+
unit: "count"
|
|
77
|
+
});
|
|
12
78
|
try {
|
|
13
|
-
const
|
|
79
|
+
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
80
|
+
enforceBodyLimit(request, maxBodyBytes);
|
|
81
|
+
await enforceAuthorization(options.authorize, "read", request, context);
|
|
82
|
+
let rawBody;
|
|
83
|
+
try {
|
|
84
|
+
rawBody = await request.json();
|
|
85
|
+
} catch {
|
|
86
|
+
throw new HandlerValidationError(400, "GRAPH_READ_REQUEST_INVALID", "Invalid graph read request.");
|
|
87
|
+
}
|
|
88
|
+
if (!isGraphQuery(rawBody)) {
|
|
89
|
+
throw new HandlerValidationError(400, "GRAPH_READ_REQUEST_INVALID", "Invalid graph read request.");
|
|
90
|
+
}
|
|
91
|
+
const query = rawBody;
|
|
14
92
|
const result = await options.gateway.execute({
|
|
15
93
|
...query,
|
|
16
94
|
traceId: query.traceId ?? context.traceContext?.traceParent
|
|
17
95
|
});
|
|
96
|
+
options.telemetry?.metric({
|
|
97
|
+
name: "graph.runtime.read.latency",
|
|
98
|
+
value: Date.now() - startedAt,
|
|
99
|
+
unit: "ms",
|
|
100
|
+
tags: { status: "200" }
|
|
101
|
+
});
|
|
18
102
|
return jsonResponse(200, result);
|
|
19
103
|
} catch (error) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
104
|
+
if (error instanceof HandlerValidationError) {
|
|
105
|
+
options.telemetry?.metric({
|
|
106
|
+
name: "graph.runtime.read.error",
|
|
107
|
+
value: 1,
|
|
108
|
+
unit: "count",
|
|
109
|
+
tags: { code: error.code }
|
|
110
|
+
});
|
|
111
|
+
options.telemetry?.error({
|
|
112
|
+
message: error.message,
|
|
113
|
+
source: "graph-runtime-azure-functions.read",
|
|
114
|
+
code: error.code
|
|
115
|
+
});
|
|
116
|
+
return jsonResponse(error.status, {
|
|
117
|
+
code: error.code,
|
|
118
|
+
message: error.message
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
options.telemetry?.metric({
|
|
122
|
+
name: "graph.runtime.read.error",
|
|
123
|
+
value: 1,
|
|
124
|
+
unit: "count",
|
|
125
|
+
tags: { code: "GRAPH_READ_UPSTREAM_FAILED" }
|
|
126
|
+
});
|
|
127
|
+
options.telemetry?.error({
|
|
128
|
+
message: "Graph read failed",
|
|
129
|
+
source: "graph-runtime-azure-functions.read",
|
|
130
|
+
code: "GRAPH_READ_UPSTREAM_FAILED"
|
|
131
|
+
});
|
|
132
|
+
return jsonResponse(502, {
|
|
133
|
+
code: "GRAPH_READ_UPSTREAM_FAILED",
|
|
134
|
+
message: "Graph read failed."
|
|
23
135
|
});
|
|
24
136
|
}
|
|
25
137
|
};
|
|
26
138
|
};
|
|
27
139
|
var createGraphWriteHandler = (options) => {
|
|
28
|
-
return async (request) => {
|
|
140
|
+
return async (request, context) => {
|
|
141
|
+
const startedAt = Date.now();
|
|
142
|
+
options.telemetry?.metric({
|
|
143
|
+
name: "graph.runtime.write.request",
|
|
144
|
+
value: 1,
|
|
145
|
+
unit: "count"
|
|
146
|
+
});
|
|
29
147
|
try {
|
|
30
|
-
const
|
|
148
|
+
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
149
|
+
enforceBodyLimit(request, maxBodyBytes);
|
|
150
|
+
await enforceAuthorization(options.authorize, "write", request, context);
|
|
151
|
+
let rawBody;
|
|
152
|
+
try {
|
|
153
|
+
rawBody = await request.json();
|
|
154
|
+
} catch {
|
|
155
|
+
throw new HandlerValidationError(400, "GRAPH_WRITE_REQUEST_INVALID", "Invalid graph write request.");
|
|
156
|
+
}
|
|
157
|
+
if (!isWriteCommandPayloadValid(rawBody)) {
|
|
158
|
+
throw new HandlerValidationError(400, "GRAPH_WRITE_REQUEST_INVALID", "Invalid graph write request.");
|
|
159
|
+
}
|
|
160
|
+
const command = rawBody;
|
|
31
161
|
const operation = await options.coordinator.submit(command);
|
|
32
162
|
const status = operation.state === "succeeded" ? 200 : 202;
|
|
163
|
+
options.telemetry?.metric({
|
|
164
|
+
name: "graph.runtime.write.latency",
|
|
165
|
+
value: Date.now() - startedAt,
|
|
166
|
+
unit: "ms",
|
|
167
|
+
tags: {
|
|
168
|
+
status: String(status),
|
|
169
|
+
state: operation.state
|
|
170
|
+
}
|
|
171
|
+
});
|
|
33
172
|
return jsonResponse(status, operation);
|
|
34
173
|
} catch (error) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
174
|
+
if (error instanceof HandlerValidationError) {
|
|
175
|
+
options.telemetry?.metric({
|
|
176
|
+
name: "graph.runtime.write.error",
|
|
177
|
+
value: 1,
|
|
178
|
+
unit: "count",
|
|
179
|
+
tags: { code: error.code }
|
|
180
|
+
});
|
|
181
|
+
options.telemetry?.error({
|
|
182
|
+
message: error.message,
|
|
183
|
+
source: "graph-runtime-azure-functions.write",
|
|
184
|
+
code: error.code
|
|
185
|
+
});
|
|
186
|
+
return jsonResponse(error.status, {
|
|
187
|
+
code: error.code,
|
|
188
|
+
message: error.message
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
options.telemetry?.metric({
|
|
192
|
+
name: "graph.runtime.write.error",
|
|
193
|
+
value: 1,
|
|
194
|
+
unit: "count",
|
|
195
|
+
tags: { code: "GRAPH_WRITE_UPSTREAM_FAILED" }
|
|
196
|
+
});
|
|
197
|
+
options.telemetry?.error({
|
|
198
|
+
message: "Graph write failed",
|
|
199
|
+
source: "graph-runtime-azure-functions.write",
|
|
200
|
+
code: "GRAPH_WRITE_UPSTREAM_FAILED"
|
|
201
|
+
});
|
|
202
|
+
return jsonResponse(503, {
|
|
203
|
+
code: "GRAPH_WRITE_UPSTREAM_FAILED",
|
|
204
|
+
message: "Graph write failed."
|
|
38
205
|
});
|
|
39
206
|
}
|
|
40
207
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/handlers.ts"],"sourcesContent":["import type { HttpHandler, HttpRequest, HttpResponseInit, InvocationContext } from \"@azure/functions\";\nimport type { GraphQuery, WriteCommand } from \"@plasius/graph-contracts\";\nimport type { GraphGateway } from \"@plasius/graph-gateway-core\";\nimport type { WriteCoordinator } from \"@plasius/graph-write-coordinator\";\n\nexport interface GraphReadHandlerOptions {\n gateway: Pick<GraphGateway, \"execute\">;\n}\n\nexport interface GraphWriteHandlerOptions {\n coordinator: Pick<WriteCoordinator, \"submit\">;\n}\n\nconst jsonResponse = (status: number, body: unknown): HttpResponseInit => ({\n status,\n jsonBody: body,\n headers: {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n },\n});\n\nexport const createGraphReadHandler = (options: GraphReadHandlerOptions): HttpHandler => {\n return async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {\n try {\n const query = (await request.json()) as GraphQuery;\n const result = await options.gateway.execute({\n ...query,\n traceId: query.traceId ?? context.traceContext?.traceParent,\n });\n return jsonResponse(200, result);\n } catch (error) {\n return jsonResponse(400, {\n code: \"GRAPH_READ_REQUEST_INVALID\",\n message: error instanceof Error ? error.message : \"Invalid read request\",\n });\n }\n };\n};\n\nexport const createGraphWriteHandler = (options: GraphWriteHandlerOptions): HttpHandler => {\n return async (request: HttpRequest): Promise<HttpResponseInit> => {\n try {\n const command = (await request.json()) as WriteCommand;\n const operation = await options.coordinator.submit(command);\n const status = operation.state === \"succeeded\" ? 200 : 202;\n return jsonResponse(status, operation);\n } catch (error) {\n return jsonResponse(400, {\n code: \"GRAPH_WRITE_REQUEST_INVALID\",\n message: error instanceof Error ? error.message : \"Invalid write request\",\n });\n }\n };\n};\n"],"mappings":";AAaA,IAAM,eAAe,CAAC,QAAgB,UAAqC;AAAA,EACzE;AAAA,EACA,UAAU;AAAA,EACV,SAAS;AAAA,IACP,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,EACnB;AACF;AAEO,IAAM,yBAAyB,CAAC,YAAkD;AACvF,SAAO,OAAO,SAAsB,YAA0D;AAC5F,QAAI;AACF,YAAM,QAAS,MAAM,QAAQ,KAAK;AAClC,YAAM,SAAS,MAAM,QAAQ,QAAQ,QAAQ;AAAA,QAC3C,GAAG;AAAA,QACH,SAAS,MAAM,WAAW,QAAQ,cAAc;AAAA,MAClD,CAAC;AACD,aAAO,aAAa,KAAK,MAAM;AAAA,IACjC,SAAS,OAAO;AACd,aAAO,aAAa,KAAK;AAAA,QACvB,MAAM;AAAA,QACN,SAAS,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MACpD,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAEO,IAAM,0BAA0B,CAAC,YAAmD;AACzF,SAAO,OAAO,YAAoD;AAChE,QAAI;AACF,YAAM,UAAW,MAAM,QAAQ,KAAK;AACpC,YAAM,YAAY,MAAM,QAAQ,YAAY,OAAO,OAAO;AAC1D,YAAM,SAAS,UAAU,UAAU,cAAc,MAAM;AACvD,aAAO,aAAa,QAAQ,SAAS;AAAA,IACvC,SAAS,OAAO;AACd,aAAO,aAAa,KAAK;AAAA,QACvB,MAAM;AAAA,QACN,SAAS,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MACpD,CAAC;AAAA,IACH;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/handlers.ts"],"sourcesContent":["import type { HttpHandler, HttpRequest, HttpResponseInit, InvocationContext } from \"@azure/functions\";\nimport { isGraphQuery, type GraphQuery, type TelemetrySink, type WriteCommand } from \"@plasius/graph-contracts\";\nimport type { GraphGateway } from \"@plasius/graph-gateway-core\";\nimport type { WriteCoordinator } from \"@plasius/graph-write-coordinator\";\n\nexport interface HandlerAuthContext {\n operation: \"read\" | \"write\";\n request: HttpRequest;\n context: InvocationContext;\n}\n\nexport type AuthorizeHandler = (context: HandlerAuthContext) => Promise<boolean> | boolean;\n\nexport interface GraphReadHandlerOptions {\n gateway: Pick<GraphGateway, \"execute\">;\n telemetry?: TelemetrySink;\n authorize?: AuthorizeHandler;\n maxBodyBytes?: number;\n}\n\nexport interface GraphWriteHandlerOptions {\n coordinator: Pick<WriteCoordinator, \"submit\">;\n telemetry?: TelemetrySink;\n authorize?: AuthorizeHandler;\n maxBodyBytes?: number;\n}\n\nconst DEFAULT_MAX_BODY_BYTES = 64 * 1024;\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n value !== null && typeof value === \"object\" && !Array.isArray(value);\n\nconst isSafeKey = (value: string): boolean => /^[A-Za-z0-9:_-]+$/.test(value);\n\nconst isWriteCommandPayloadValid = (value: unknown): value is WriteCommand => {\n if (!isObject(value)) {\n return false;\n }\n\n if (\n typeof value.idempotencyKey !== \"string\"\n || value.idempotencyKey.length === 0\n || value.idempotencyKey.length > 128\n || !isSafeKey(value.idempotencyKey)\n ) {\n return false;\n }\n\n if (\n typeof value.partitionKey !== \"string\"\n || value.partitionKey.length === 0\n || value.partitionKey.length > 128\n || !isSafeKey(value.partitionKey)\n ) {\n return false;\n }\n\n if (\n typeof value.aggregateKey !== \"string\"\n || value.aggregateKey.length === 0\n || value.aggregateKey.length > 256\n || !isSafeKey(value.aggregateKey)\n ) {\n return false;\n }\n\n if (!isObject(value.payload)) {\n return false;\n }\n\n if (typeof value.submittedAtEpochMs !== \"number\" || !Number.isFinite(value.submittedAtEpochMs) || value.submittedAtEpochMs < 0) {\n return false;\n }\n\n return value.actorId === undefined || (typeof value.actorId === \"string\" && value.actorId.length > 0 && value.actorId.length <= 128);\n};\n\nclass HandlerValidationError extends Error {\n public readonly status: number;\n public readonly code: string;\n\n public constructor(status: number, code: string, message: string) {\n super(message);\n this.status = status;\n this.code = code;\n }\n}\n\nconst jsonResponse = (status: number, body: unknown): HttpResponseInit => ({\n status,\n jsonBody: body,\n headers: {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n },\n});\n\nconst getContentLength = (request: HttpRequest): number | null => {\n const raw = request.headers?.get?.(\"content-length\");\n if (!raw) {\n return null;\n }\n\n const parsed = Number.parseInt(raw, 10);\n if (!Number.isFinite(parsed) || parsed < 0) {\n return null;\n }\n\n return parsed;\n};\n\nconst enforceBodyLimit = (request: HttpRequest, maxBodyBytes: number): void => {\n const contentLength = getContentLength(request);\n if (contentLength !== null && contentLength > maxBodyBytes) {\n throw new HandlerValidationError(413, \"GRAPH_REQUEST_BODY_TOO_LARGE\", \"Request payload exceeds allowed limit.\");\n }\n};\n\nconst enforceAuthorization = async (\n authorize: AuthorizeHandler | undefined,\n operation: \"read\" | \"write\",\n request: HttpRequest,\n context: InvocationContext,\n): Promise<void> => {\n if (!authorize) {\n return;\n }\n\n const allowed = await authorize({ operation, request, context });\n if (!allowed) {\n throw new HandlerValidationError(403, \"GRAPH_FORBIDDEN\", \"Forbidden\");\n }\n};\n\nexport const createGraphReadHandler = (options: GraphReadHandlerOptions): HttpHandler => {\n return async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {\n const startedAt = Date.now();\n options.telemetry?.metric({\n name: \"graph.runtime.read.request\",\n value: 1,\n unit: \"count\",\n });\n try {\n const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;\n enforceBodyLimit(request, maxBodyBytes);\n await enforceAuthorization(options.authorize, \"read\", request, context);\n\n let rawBody: unknown;\n try {\n rawBody = await request.json();\n } catch {\n throw new HandlerValidationError(400, \"GRAPH_READ_REQUEST_INVALID\", \"Invalid graph read request.\");\n }\n if (!isGraphQuery(rawBody)) {\n throw new HandlerValidationError(400, \"GRAPH_READ_REQUEST_INVALID\", \"Invalid graph read request.\");\n }\n\n const query = rawBody as GraphQuery;\n const result = await options.gateway.execute({\n ...query,\n traceId: query.traceId ?? context.traceContext?.traceParent,\n });\n options.telemetry?.metric({\n name: \"graph.runtime.read.latency\",\n value: Date.now() - startedAt,\n unit: \"ms\",\n tags: { status: \"200\" },\n });\n return jsonResponse(200, result);\n } catch (error) {\n if (error instanceof HandlerValidationError) {\n options.telemetry?.metric({\n name: \"graph.runtime.read.error\",\n value: 1,\n unit: \"count\",\n tags: { code: error.code },\n });\n options.telemetry?.error({\n message: error.message,\n source: \"graph-runtime-azure-functions.read\",\n code: error.code,\n });\n return jsonResponse(error.status, {\n code: error.code,\n message: error.message,\n });\n }\n\n options.telemetry?.metric({\n name: \"graph.runtime.read.error\",\n value: 1,\n unit: \"count\",\n tags: { code: \"GRAPH_READ_UPSTREAM_FAILED\" },\n });\n options.telemetry?.error({\n message: \"Graph read failed\",\n source: \"graph-runtime-azure-functions.read\",\n code: \"GRAPH_READ_UPSTREAM_FAILED\",\n });\n return jsonResponse(502, {\n code: \"GRAPH_READ_UPSTREAM_FAILED\",\n message: \"Graph read failed.\",\n });\n }\n };\n};\n\nexport const createGraphWriteHandler = (options: GraphWriteHandlerOptions): HttpHandler => {\n return async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {\n const startedAt = Date.now();\n options.telemetry?.metric({\n name: \"graph.runtime.write.request\",\n value: 1,\n unit: \"count\",\n });\n try {\n const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;\n enforceBodyLimit(request, maxBodyBytes);\n await enforceAuthorization(options.authorize, \"write\", request, context);\n\n let rawBody: unknown;\n try {\n rawBody = await request.json();\n } catch {\n throw new HandlerValidationError(400, \"GRAPH_WRITE_REQUEST_INVALID\", \"Invalid graph write request.\");\n }\n if (!isWriteCommandPayloadValid(rawBody)) {\n throw new HandlerValidationError(400, \"GRAPH_WRITE_REQUEST_INVALID\", \"Invalid graph write request.\");\n }\n\n const command = rawBody as WriteCommand;\n const operation = await options.coordinator.submit(command);\n const status = operation.state === \"succeeded\" ? 200 : 202;\n options.telemetry?.metric({\n name: \"graph.runtime.write.latency\",\n value: Date.now() - startedAt,\n unit: \"ms\",\n tags: {\n status: String(status),\n state: operation.state,\n },\n });\n return jsonResponse(status, operation);\n } catch (error) {\n if (error instanceof HandlerValidationError) {\n options.telemetry?.metric({\n name: \"graph.runtime.write.error\",\n value: 1,\n unit: \"count\",\n tags: { code: error.code },\n });\n options.telemetry?.error({\n message: error.message,\n source: \"graph-runtime-azure-functions.write\",\n code: error.code,\n });\n return jsonResponse(error.status, {\n code: error.code,\n message: error.message,\n });\n }\n\n options.telemetry?.metric({\n name: \"graph.runtime.write.error\",\n value: 1,\n unit: \"count\",\n tags: { code: \"GRAPH_WRITE_UPSTREAM_FAILED\" },\n });\n options.telemetry?.error({\n message: \"Graph write failed\",\n source: \"graph-runtime-azure-functions.write\",\n code: \"GRAPH_WRITE_UPSTREAM_FAILED\",\n });\n return jsonResponse(503, {\n code: \"GRAPH_WRITE_UPSTREAM_FAILED\",\n message: \"Graph write failed.\",\n });\n }\n };\n};\n"],"mappings":"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","names":[]}
|