@uns-kit/api 2.0.44 → 2.0.46
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 +145 -0
- package/dist/api-event-routing.d.ts +39 -0
- package/dist/api-event-routing.js +88 -0
- package/dist/api-interfaces.d.ts +185 -1
- package/dist/api-interfaces.js +311 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/parquet.d.ts +8 -0
- package/dist/parquet.js +61 -0
- package/dist/uns-api-plugin.js +9 -0
- package/dist/uns-api-proxy.d.ts +18 -8
- package/dist/uns-api-proxy.js +227 -17
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -117,6 +117,151 @@ api.event.on("apiPostEvent", (event: UnsEvents["apiPostEvent"]) => {
|
|
|
117
117
|
});
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
## Data offer example
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import "@uns-kit/api";
|
|
124
|
+
import {
|
|
125
|
+
defineDataCatalogField,
|
|
126
|
+
defineDataCatalogOfferSource,
|
|
127
|
+
defineDataCatalogQueryParam,
|
|
128
|
+
defineDataCatalogSchema,
|
|
129
|
+
defineServiceApi,
|
|
130
|
+
projectRowsForDataCatalogSchema,
|
|
131
|
+
registerApiCatalog,
|
|
132
|
+
type UnsProxyProcessWithApi,
|
|
133
|
+
} from "@uns-kit/api";
|
|
134
|
+
import { ConfigFile, UnsProxyProcess } from "@uns-kit/core";
|
|
135
|
+
|
|
136
|
+
const config = await ConfigFile.loadConfig();
|
|
137
|
+
const proc = new UnsProxyProcess(config.infra.host!, {
|
|
138
|
+
processName: config.uns.processName,
|
|
139
|
+
}) as UnsProxyProcessWithApi;
|
|
140
|
+
|
|
141
|
+
const api = await proc.createApiProxy("catalog", {
|
|
142
|
+
jwks: { wellKnownJwksUrl: config.uns.jwksWellKnownUrl! },
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const orderRows = [
|
|
146
|
+
{ ORDER_ID: "PO-1001", STATUS: "released" },
|
|
147
|
+
{ ORDER_ID: "PO-1002", STATUS: "finished" },
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
const productionOrdersSchema = defineDataCatalogSchema({
|
|
151
|
+
id: "production-orders",
|
|
152
|
+
title: "Production Orders",
|
|
153
|
+
contentType: "application/json",
|
|
154
|
+
fields: [
|
|
155
|
+
defineDataCatalogField("count", "number", "Number of returned rows"),
|
|
156
|
+
defineDataCatalogField("data", "array", "Returned rows"),
|
|
157
|
+
defineDataCatalogField("orderId", "string", "Order id", { sourceKey: "ORDER_ID" }),
|
|
158
|
+
defineDataCatalogField("status", "string", "Order status", { sourceKey: "STATUS" }),
|
|
159
|
+
],
|
|
160
|
+
examplePayloads: [
|
|
161
|
+
{
|
|
162
|
+
count: 1,
|
|
163
|
+
data: [{ orderId: "PO-1001", status: "released" }],
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const productionOrderRowSchema = defineDataCatalogSchema({
|
|
169
|
+
id: "production-orders-row",
|
|
170
|
+
title: "Production Order Row",
|
|
171
|
+
contentType: "application/json",
|
|
172
|
+
fields: productionOrdersSchema.fields?.filter((field) => field.name !== "count" && field.name !== "data") ?? [],
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const serviceApis = {
|
|
176
|
+
status: defineServiceApi({
|
|
177
|
+
attribute: "status",
|
|
178
|
+
method: "GET",
|
|
179
|
+
description: "Service status endpoint",
|
|
180
|
+
tags: ["service"],
|
|
181
|
+
handler: async (event: any) => {
|
|
182
|
+
event.res.json({ status: "ok" });
|
|
183
|
+
},
|
|
184
|
+
}),
|
|
185
|
+
command: defineServiceApi({
|
|
186
|
+
attribute: "command",
|
|
187
|
+
method: "POST",
|
|
188
|
+
description: "Service command endpoint",
|
|
189
|
+
tags: ["service"],
|
|
190
|
+
requestBody: {
|
|
191
|
+
required: true,
|
|
192
|
+
description: "Command payload",
|
|
193
|
+
contentType: "application/json",
|
|
194
|
+
schemas: [
|
|
195
|
+
defineDataCatalogSchema({
|
|
196
|
+
id: "service-command-request",
|
|
197
|
+
title: "Service Command Request",
|
|
198
|
+
contentType: "application/json",
|
|
199
|
+
fields: [
|
|
200
|
+
defineDataCatalogField("command", "string", "Command name", { required: true, example: "refresh-cache" }),
|
|
201
|
+
defineDataCatalogField("target", "string", "Optional target", { example: "orders" }),
|
|
202
|
+
],
|
|
203
|
+
}),
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
handler: async (event: any) => {
|
|
207
|
+
const { command, target } = event.req.body ?? {};
|
|
208
|
+
if (!command) {
|
|
209
|
+
return event.res.status(400).json({ error: "Missing command" });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
event.res.json({
|
|
213
|
+
status: "accepted",
|
|
214
|
+
received: {
|
|
215
|
+
command,
|
|
216
|
+
...(target ? { target } : {}),
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
}),
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const dataOfferSources = {
|
|
224
|
+
productionOrders: defineDataCatalogOfferSource({
|
|
225
|
+
offerId: "production-orders",
|
|
226
|
+
topic: "factory/demo/app",
|
|
227
|
+
asset: "orders",
|
|
228
|
+
objectType: "dataset",
|
|
229
|
+
objectId: "production",
|
|
230
|
+
attribute: "list",
|
|
231
|
+
displayName: "Production Orders",
|
|
232
|
+
description: "Browse production orders.",
|
|
233
|
+
method: "GET",
|
|
234
|
+
tags: ["orders"],
|
|
235
|
+
queryParams: [
|
|
236
|
+
defineDataCatalogQueryParam("status", "Optional status filter"),
|
|
237
|
+
],
|
|
238
|
+
schema: productionOrdersSchema,
|
|
239
|
+
response: {
|
|
240
|
+
statusCode: "200",
|
|
241
|
+
contentType: "application/json",
|
|
242
|
+
},
|
|
243
|
+
handler: async (event: any) => {
|
|
244
|
+
const statusFilter = String(event.req.query.status ?? "").trim().toLowerCase();
|
|
245
|
+
const filteredRows = statusFilter
|
|
246
|
+
? orderRows.filter((row) => row.STATUS.toLowerCase() === statusFilter)
|
|
247
|
+
: orderRows;
|
|
248
|
+
const data = projectRowsForDataCatalogSchema(filteredRows, productionOrderRowSchema);
|
|
249
|
+
|
|
250
|
+
event.res.json({
|
|
251
|
+
count: data.length,
|
|
252
|
+
data,
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
}),
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
await registerApiCatalog(api, {
|
|
259
|
+
serviceApis,
|
|
260
|
+
dataOfferSources,
|
|
261
|
+
context: undefined,
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
120
265
|
## Endpoint signature
|
|
121
266
|
|
|
122
267
|
```
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { UnsEvents } from "@uns-kit/core/uns/uns-interfaces.js";
|
|
2
|
+
import type { ApiInteractionDefinition, ApiInteractionMethod, DataCatalogOfferRegistration, DataCatalogOfferSourceRegistration, ServiceApiRegistration } from "./api-interfaces.js";
|
|
3
|
+
import type UnsApiProxy from "./uns-api-proxy.js";
|
|
4
|
+
type ApiEventEnvelope = {
|
|
5
|
+
req: any;
|
|
6
|
+
res: any;
|
|
7
|
+
};
|
|
8
|
+
type ApiEventByMethod = {
|
|
9
|
+
GET: UnsEvents["apiGetEvent"];
|
|
10
|
+
POST: UnsEvents["apiPostEvent"];
|
|
11
|
+
PUT: ApiEventEnvelope;
|
|
12
|
+
PATCH: ApiEventEnvelope;
|
|
13
|
+
DELETE: ApiEventEnvelope;
|
|
14
|
+
};
|
|
15
|
+
export type ApiHandler<Method extends ApiInteractionMethod = ApiInteractionMethod, Context = void> = (event: ApiEventByMethod[Method], context: Context) => Promise<void>;
|
|
16
|
+
export type RegisterApiEventsOptions = {
|
|
17
|
+
notFoundMessage?: string;
|
|
18
|
+
onError?: (input: {
|
|
19
|
+
method: ApiInteractionMethod;
|
|
20
|
+
reqPath?: string;
|
|
21
|
+
error: unknown;
|
|
22
|
+
}) => void;
|
|
23
|
+
};
|
|
24
|
+
export type RegisterApiCatalogOptions<Context, Handler, ServiceApis extends Record<string, ServiceApiRegistration<Handler>>, OfferSources extends Record<string, DataCatalogOfferSourceRegistration<Handler>>> = {
|
|
25
|
+
serviceApis?: ServiceApis;
|
|
26
|
+
dataOfferSources?: OfferSources;
|
|
27
|
+
context: Context;
|
|
28
|
+
dataCatalogOffers?: DataCatalogOfferRegistration[];
|
|
29
|
+
options?: RegisterApiEventsOptions;
|
|
30
|
+
};
|
|
31
|
+
export declare function normalizeApiRequestPath(path?: string): string | undefined;
|
|
32
|
+
export declare function registerApiCatalog<Context, Handler, ServiceApis extends Record<string, ServiceApiRegistration<Handler>>, OfferSources extends Record<string, DataCatalogOfferSourceRegistration<Handler>>>(apiInput: UnsApiProxy, input: RegisterApiCatalogOptions<Context, Handler, ServiceApis, OfferSources>): Promise<{
|
|
33
|
+
serviceApiRoutes: ApiInteractionDefinition<Handler>[];
|
|
34
|
+
dataOfferRoutes: ApiInteractionDefinition<Handler>[];
|
|
35
|
+
dataCatalogOffers: DataCatalogOfferRegistration[];
|
|
36
|
+
}>;
|
|
37
|
+
export declare function registerApiInteractions<Handler>(apiInput: UnsApiProxy, routes: ApiInteractionDefinition<Handler>[]): Promise<void>;
|
|
38
|
+
export declare function registerApiEvents<Context, Handler>(apiInput: UnsApiProxy, routes: ApiInteractionDefinition<Handler>[], context: Context, options?: RegisterApiEventsOptions): void;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { buildApiInteractionTopicPath, buildApiInteractionsFromDataOfferSources, buildDataCatalogOffersFromSources, buildServiceApiInteractions, } from "./api-interfaces.js";
|
|
2
|
+
const EVENT_NAME_BY_METHOD = {
|
|
3
|
+
GET: "apiGetEvent",
|
|
4
|
+
POST: "apiPostEvent",
|
|
5
|
+
PUT: "apiPutEvent",
|
|
6
|
+
PATCH: "apiPatchEvent",
|
|
7
|
+
DELETE: "apiDeleteEvent",
|
|
8
|
+
};
|
|
9
|
+
export function normalizeApiRequestPath(path) {
|
|
10
|
+
const normalized = path?.replace(/^\/+|\/+$/g, "");
|
|
11
|
+
return normalized?.length ? normalized : undefined;
|
|
12
|
+
}
|
|
13
|
+
export async function registerApiCatalog(apiInput, input) {
|
|
14
|
+
const serviceApiRoutes = Object.values(buildServiceApiInteractions(apiInput.getProcessName(), input.serviceApis ?? {}));
|
|
15
|
+
const dataOfferRoutes = Object.values(buildApiInteractionsFromDataOfferSources(input.dataOfferSources ?? {}));
|
|
16
|
+
const dataCatalogOffers = input.dataCatalogOffers ?? buildDataCatalogOffersFromSources(input.dataOfferSources ?? {});
|
|
17
|
+
const allRoutes = [...serviceApiRoutes, ...dataOfferRoutes];
|
|
18
|
+
await registerApiInteractions(apiInput, allRoutes);
|
|
19
|
+
for (const offer of dataCatalogOffers) {
|
|
20
|
+
apiInput.registerDataOffer(offer);
|
|
21
|
+
}
|
|
22
|
+
registerApiEvents(apiInput, allRoutes, input.context, input.options);
|
|
23
|
+
return { serviceApiRoutes, dataOfferRoutes, dataCatalogOffers };
|
|
24
|
+
}
|
|
25
|
+
export async function registerApiInteractions(apiInput, routes) {
|
|
26
|
+
for (const route of routes) {
|
|
27
|
+
switch (route.method) {
|
|
28
|
+
case "GET":
|
|
29
|
+
await apiInput.get(route.topic, route.asset, route.objectType, route.objectId, route.attribute, route.options);
|
|
30
|
+
break;
|
|
31
|
+
case "POST":
|
|
32
|
+
await apiInput.post(route.topic, route.asset, route.objectType, route.objectId, route.attribute, route.options);
|
|
33
|
+
break;
|
|
34
|
+
case "PUT":
|
|
35
|
+
await apiInput.put(route.topic, route.asset, route.objectType, route.objectId, route.attribute, route.options);
|
|
36
|
+
break;
|
|
37
|
+
case "PATCH":
|
|
38
|
+
await apiInput.patch(route.topic, route.asset, route.objectType, route.objectId, route.attribute, route.options);
|
|
39
|
+
break;
|
|
40
|
+
case "DELETE":
|
|
41
|
+
await apiInput.delete(route.topic, route.asset, route.objectType, route.objectId, route.attribute, route.options);
|
|
42
|
+
break;
|
|
43
|
+
default:
|
|
44
|
+
throw new Error(`Unsupported API method: ${route.method}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function registerApiEvents(apiInput, routes, context, options = {}) {
|
|
49
|
+
const handlersByMethod = buildRouteHandlerIndex(routes);
|
|
50
|
+
bindMethodEvent(apiInput, "GET", handlersByMethod.GET, context, options);
|
|
51
|
+
bindMethodEvent(apiInput, "POST", handlersByMethod.POST, context, options);
|
|
52
|
+
bindMethodEvent(apiInput, "PUT", handlersByMethod.PUT, context, options);
|
|
53
|
+
bindMethodEvent(apiInput, "PATCH", handlersByMethod.PATCH, context, options);
|
|
54
|
+
bindMethodEvent(apiInput, "DELETE", handlersByMethod.DELETE, context, options);
|
|
55
|
+
}
|
|
56
|
+
function buildRouteHandlerIndex(routes) {
|
|
57
|
+
return routes.reduce((acc, route) => {
|
|
58
|
+
acc[route.method][buildApiInteractionTopicPath(route)] = route.handler;
|
|
59
|
+
return acc;
|
|
60
|
+
}, {
|
|
61
|
+
GET: {},
|
|
62
|
+
POST: {},
|
|
63
|
+
PUT: {},
|
|
64
|
+
PATCH: {},
|
|
65
|
+
DELETE: {},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function bindMethodEvent(apiInput, method, handlers, context, options) {
|
|
69
|
+
const eventName = EVENT_NAME_BY_METHOD[method];
|
|
70
|
+
apiInput.event.on(eventName, async (event) => {
|
|
71
|
+
const reqPath = normalizeApiRequestPath(event.req.path);
|
|
72
|
+
const handler = reqPath ? handlers[reqPath] : undefined;
|
|
73
|
+
await executeHandler(event, handler, context, method, reqPath, options);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async function executeHandler(event, handler, context, method, reqPath, options) {
|
|
77
|
+
if (!handler) {
|
|
78
|
+
event.res.status(404).send(options.notFoundMessage ?? "API handler not found");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
await handler(event, context);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
options.onError?.({ method, reqPath, error });
|
|
86
|
+
event.res.status(500).send("Server error");
|
|
87
|
+
}
|
|
88
|
+
}
|
package/dist/api-interfaces.d.ts
CHANGED
|
@@ -1 +1,185 @@
|
|
|
1
|
-
export type { IApiProxyOptions, IGetEndpointOptions, QueryParamDef } from "@uns-kit/core/uns/uns-interfaces.js";
|
|
1
|
+
export type { IApiProxyOptions, IGetEndpointOptions, IPostEndpointOptions, QueryParamDef, } from "@uns-kit/core/uns/uns-interfaces.js";
|
|
2
|
+
import type { IGetEndpointOptions, IPostEndpointOptions } from "@uns-kit/core/uns/uns-interfaces.js";
|
|
3
|
+
export type ApiInteractionMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
4
|
+
export type DataCatalogParameterRegistration = {
|
|
5
|
+
name: string;
|
|
6
|
+
in: "path" | "query" | "header" | "cookie";
|
|
7
|
+
required?: boolean;
|
|
8
|
+
description?: string | null;
|
|
9
|
+
type?: string | null;
|
|
10
|
+
format?: string | null;
|
|
11
|
+
nullable?: boolean;
|
|
12
|
+
example?: unknown;
|
|
13
|
+
enumValues?: string[];
|
|
14
|
+
schema?: unknown;
|
|
15
|
+
};
|
|
16
|
+
export type DataCatalogSchemaFieldRegistration = {
|
|
17
|
+
name: string;
|
|
18
|
+
path?: string;
|
|
19
|
+
sourceKey?: string | null;
|
|
20
|
+
type?: string | null;
|
|
21
|
+
format?: string | null;
|
|
22
|
+
nullable?: boolean;
|
|
23
|
+
required?: boolean;
|
|
24
|
+
description?: string | null;
|
|
25
|
+
enumValues?: string[];
|
|
26
|
+
example?: unknown;
|
|
27
|
+
};
|
|
28
|
+
export type DataCatalogSchemaRegistration = {
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
kind?: string | null;
|
|
32
|
+
source?: string | null;
|
|
33
|
+
contentType?: string | null;
|
|
34
|
+
rootType?: string | null;
|
|
35
|
+
fieldPathPrefix?: string | null;
|
|
36
|
+
nullable?: boolean;
|
|
37
|
+
description?: string | null;
|
|
38
|
+
fields?: DataCatalogSchemaFieldRegistration[];
|
|
39
|
+
examplePayloads?: unknown[];
|
|
40
|
+
};
|
|
41
|
+
export type DataCatalogRequestBodyRegistration = {
|
|
42
|
+
required?: boolean;
|
|
43
|
+
description?: string | null;
|
|
44
|
+
contentType?: string | null;
|
|
45
|
+
schemas?: DataCatalogSchemaRegistration[];
|
|
46
|
+
schemaIds?: string[];
|
|
47
|
+
};
|
|
48
|
+
export type DataCatalogResponseRegistration = {
|
|
49
|
+
statusCode: string;
|
|
50
|
+
description?: string | null;
|
|
51
|
+
contentType?: string | null;
|
|
52
|
+
schemas?: DataCatalogSchemaRegistration[];
|
|
53
|
+
schemaIds?: string[];
|
|
54
|
+
examplePayloads?: unknown[];
|
|
55
|
+
headers?: DataCatalogParameterRegistration[];
|
|
56
|
+
};
|
|
57
|
+
export type DataCatalogOperationRegistration = {
|
|
58
|
+
id?: string;
|
|
59
|
+
method: ApiInteractionMethod | "HEAD" | "OPTIONS";
|
|
60
|
+
path: string;
|
|
61
|
+
summary?: string | null;
|
|
62
|
+
description?: string | null;
|
|
63
|
+
tags?: string[];
|
|
64
|
+
deprecated?: boolean;
|
|
65
|
+
parameters?: DataCatalogParameterRegistration[];
|
|
66
|
+
headers?: DataCatalogParameterRegistration[];
|
|
67
|
+
requestBody?: DataCatalogRequestBodyRegistration | null;
|
|
68
|
+
responses?: DataCatalogResponseRegistration[];
|
|
69
|
+
};
|
|
70
|
+
export type DataCatalogOfferRegistration = {
|
|
71
|
+
offerId: string;
|
|
72
|
+
displayName: string;
|
|
73
|
+
description?: string | null;
|
|
74
|
+
owner?: string | null;
|
|
75
|
+
status?: string | null;
|
|
76
|
+
tags?: string[];
|
|
77
|
+
categories?: string[];
|
|
78
|
+
microserviceName?: string | null;
|
|
79
|
+
version?: string | null;
|
|
80
|
+
basePaths?: string[];
|
|
81
|
+
operations: DataCatalogOperationRegistration[];
|
|
82
|
+
schemas?: DataCatalogSchemaRegistration[];
|
|
83
|
+
swaggerPath?: string | null;
|
|
84
|
+
metadata?: Record<string, unknown> | null;
|
|
85
|
+
};
|
|
86
|
+
type ApiRouteIdentity<Handler = unknown> = {
|
|
87
|
+
topic: string;
|
|
88
|
+
asset: string;
|
|
89
|
+
objectType: string;
|
|
90
|
+
objectId: string;
|
|
91
|
+
attribute: string;
|
|
92
|
+
handler: Handler;
|
|
93
|
+
};
|
|
94
|
+
export type DataCatalogOfferSourceRegistration<Handler = unknown> = ApiRouteIdentity<Handler> & {
|
|
95
|
+
offerId: string;
|
|
96
|
+
displayName: string;
|
|
97
|
+
description: string;
|
|
98
|
+
owner?: string;
|
|
99
|
+
status?: string;
|
|
100
|
+
tags?: string[];
|
|
101
|
+
categories?: string[];
|
|
102
|
+
method?: ApiInteractionMethod;
|
|
103
|
+
queryParams?: DataCatalogParameterRegistration[];
|
|
104
|
+
headers?: DataCatalogParameterRegistration[];
|
|
105
|
+
requestBody?: DataCatalogRequestBodyRegistration | null;
|
|
106
|
+
response?: {
|
|
107
|
+
statusCode?: string;
|
|
108
|
+
description?: string | null;
|
|
109
|
+
contentType?: string | null;
|
|
110
|
+
headers?: DataCatalogParameterRegistration[];
|
|
111
|
+
examplePayloads?: unknown[];
|
|
112
|
+
};
|
|
113
|
+
schema: DataCatalogSchemaRegistration;
|
|
114
|
+
apiDescription?: string;
|
|
115
|
+
};
|
|
116
|
+
export type ServiceApiRegistration<Handler = unknown> = {
|
|
117
|
+
attribute: string;
|
|
118
|
+
handler: Handler;
|
|
119
|
+
method?: ApiInteractionMethod;
|
|
120
|
+
description: string;
|
|
121
|
+
publishInUnsTree?: boolean;
|
|
122
|
+
tags?: string[];
|
|
123
|
+
queryParams?: DataCatalogParameterRegistration[];
|
|
124
|
+
requestBody?: DataCatalogRequestBodyRegistration | null;
|
|
125
|
+
response?: DataCatalogResponseRegistration;
|
|
126
|
+
schema?: DataCatalogSchemaRegistration;
|
|
127
|
+
topic?: string;
|
|
128
|
+
asset?: string;
|
|
129
|
+
objectType?: string;
|
|
130
|
+
objectId?: string;
|
|
131
|
+
};
|
|
132
|
+
export type ApiInteractionDefinition<Handler = unknown> = {
|
|
133
|
+
id: string;
|
|
134
|
+
topic: string;
|
|
135
|
+
asset: string;
|
|
136
|
+
objectType: string;
|
|
137
|
+
objectId: string;
|
|
138
|
+
attribute: string;
|
|
139
|
+
method: ApiInteractionMethod;
|
|
140
|
+
routeOnly?: boolean;
|
|
141
|
+
registryTopic?: "api-endpoints" | "service-endpoints" | "data-offer-endpoints";
|
|
142
|
+
options: IGetEndpointOptions | IPostEndpointOptions;
|
|
143
|
+
handler: Handler;
|
|
144
|
+
};
|
|
145
|
+
export declare function defineApiInteraction<Handler>(input: ApiInteractionDefinition<Handler>): ApiInteractionDefinition<Handler>;
|
|
146
|
+
export declare function defineServiceApi<Handler>(input: ServiceApiRegistration<Handler>): ServiceApiRegistration<Handler>;
|
|
147
|
+
export declare function buildApiInteractionPath(route: Pick<ApiInteractionDefinition, "topic" | "asset" | "objectType" | "objectId" | "attribute">, apiBasePrefix?: string): string;
|
|
148
|
+
export declare function buildApiInteractionTopicPath(route: Pick<ApiInteractionDefinition, "topic" | "asset" | "objectType" | "objectId" | "attribute">): string;
|
|
149
|
+
export declare function defineDataCatalogQueryParam(name: string, description: string, options?: {
|
|
150
|
+
required?: boolean;
|
|
151
|
+
type?: string;
|
|
152
|
+
format?: string;
|
|
153
|
+
example?: unknown;
|
|
154
|
+
}): DataCatalogParameterRegistration;
|
|
155
|
+
export declare function defineDataCatalogField(name: string, type: string, description: string, options?: {
|
|
156
|
+
path?: string;
|
|
157
|
+
sourceKey?: string;
|
|
158
|
+
required?: boolean;
|
|
159
|
+
format?: string;
|
|
160
|
+
nullable?: boolean;
|
|
161
|
+
example?: unknown;
|
|
162
|
+
enumValues?: string[];
|
|
163
|
+
}): DataCatalogSchemaFieldRegistration;
|
|
164
|
+
export declare function defineDataCatalogSchema(input: {
|
|
165
|
+
id: string;
|
|
166
|
+
title: string;
|
|
167
|
+
contentType: string;
|
|
168
|
+
description?: string;
|
|
169
|
+
kind?: string;
|
|
170
|
+
source?: string;
|
|
171
|
+
rootType?: string;
|
|
172
|
+
fieldPathPrefix?: string;
|
|
173
|
+
fields: DataCatalogSchemaFieldRegistration[];
|
|
174
|
+
examplePayloads?: unknown[];
|
|
175
|
+
}): DataCatalogSchemaRegistration;
|
|
176
|
+
export declare function buildDataCatalogOfferOperation(route: Pick<ApiInteractionDefinition, "topic" | "asset" | "objectType" | "objectId" | "attribute">, operation: Omit<DataCatalogOperationRegistration, "path">, apiBasePrefix?: string): DataCatalogOperationRegistration;
|
|
177
|
+
export declare function defineDataCatalogOfferSource<Handler>(input: DataCatalogOfferSourceRegistration<Handler>): DataCatalogOfferSourceRegistration<Handler>;
|
|
178
|
+
export declare function buildServiceApiInteractions<Handler, T extends Record<string, ServiceApiRegistration<Handler>>>(processName: string, definitions: T): Record<keyof T, ApiInteractionDefinition<Handler>>;
|
|
179
|
+
export declare function buildApiInteractionsFromDataOfferSources<Handler, T extends Record<string, DataCatalogOfferSourceRegistration<Handler>>>(sources: T): Record<keyof T, ApiInteractionDefinition<Handler>>;
|
|
180
|
+
export declare function buildDataCatalogOffersFromSources<T extends Record<string, DataCatalogOfferSourceRegistration<any>>>(sources: T): DataCatalogOfferRegistration[];
|
|
181
|
+
export declare function projectRowsForDataCatalogSchema(rows: Array<Record<string, unknown>>, schema: DataCatalogSchemaRegistration): Array<Record<string, unknown>>;
|
|
182
|
+
export declare function buildSchemaFieldPathIndex(schema: DataCatalogSchemaRegistration): Array<{
|
|
183
|
+
path: string;
|
|
184
|
+
name: string;
|
|
185
|
+
}>;
|
package/dist/api-interfaces.js
CHANGED
|
@@ -1 +1,311 @@
|
|
|
1
|
-
|
|
1
|
+
import { buildUnsRoutePath } from "@uns-kit/core/uns/uns-path.js";
|
|
2
|
+
export function defineApiInteraction(input) {
|
|
3
|
+
return input;
|
|
4
|
+
}
|
|
5
|
+
export function defineServiceApi(input) {
|
|
6
|
+
return input;
|
|
7
|
+
}
|
|
8
|
+
export function buildApiInteractionPath(route, apiBasePrefix = "/api") {
|
|
9
|
+
return `${apiBasePrefix}${buildUnsRoutePath(route.topic, route.asset, route.objectType, route.objectId, route.attribute)}`.replace(/\/{2,}/g, "/");
|
|
10
|
+
}
|
|
11
|
+
export function buildApiInteractionTopicPath(route) {
|
|
12
|
+
return buildUnsRoutePath(route.topic, route.asset, route.objectType, route.objectId, route.attribute).slice(1);
|
|
13
|
+
}
|
|
14
|
+
export function defineDataCatalogQueryParam(name, description, options = {}) {
|
|
15
|
+
return {
|
|
16
|
+
name,
|
|
17
|
+
in: "query",
|
|
18
|
+
type: options.type ?? "string",
|
|
19
|
+
required: options.required === true,
|
|
20
|
+
description,
|
|
21
|
+
...(options.format ? { format: options.format } : {}),
|
|
22
|
+
...(options.example !== undefined ? { example: options.example } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function defineDataCatalogField(name, type, description, options = {}) {
|
|
26
|
+
return {
|
|
27
|
+
name,
|
|
28
|
+
path: options.path ?? name,
|
|
29
|
+
type,
|
|
30
|
+
description,
|
|
31
|
+
required: options.required === true,
|
|
32
|
+
nullable: options.nullable === true,
|
|
33
|
+
...(options.format ? { format: options.format } : {}),
|
|
34
|
+
...(options.example !== undefined ? { example: options.example } : {}),
|
|
35
|
+
...(Array.isArray(options.enumValues) ? { enumValues: options.enumValues } : {}),
|
|
36
|
+
...(options.sourceKey ? { sourceKey: options.sourceKey } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function defineDataCatalogSchema(input) {
|
|
40
|
+
const fieldPathPrefix = input.fieldPathPrefix?.trim();
|
|
41
|
+
const fields = input.fields.map((field) => ({
|
|
42
|
+
...field,
|
|
43
|
+
path: field.path ?? buildSchemaFieldPath(field.name, field.type ?? "string", fieldPathPrefix),
|
|
44
|
+
}));
|
|
45
|
+
const shouldAutogenerateExamples = !isBinaryCatalogContentType(input.contentType);
|
|
46
|
+
const examplePayloads = Array.isArray(input.examplePayloads) && input.examplePayloads.length
|
|
47
|
+
? input.examplePayloads
|
|
48
|
+
: shouldAutogenerateExamples
|
|
49
|
+
? buildExamplePayloadsFromFields(fields)
|
|
50
|
+
: [];
|
|
51
|
+
return {
|
|
52
|
+
id: input.id,
|
|
53
|
+
title: input.title,
|
|
54
|
+
kind: input.kind ?? "response",
|
|
55
|
+
source: input.source ?? "registered",
|
|
56
|
+
contentType: input.contentType,
|
|
57
|
+
rootType: input.rootType ?? inferSchemaRootType(input.contentType, input.fields),
|
|
58
|
+
...(fieldPathPrefix ? { fieldPathPrefix } : {}),
|
|
59
|
+
...(input.description ? { description: input.description } : {}),
|
|
60
|
+
fields,
|
|
61
|
+
...(examplePayloads?.length ? { examplePayloads } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function isBinaryCatalogContentType(contentType) {
|
|
65
|
+
const normalized = String(contentType ?? "").toLowerCase();
|
|
66
|
+
return normalized.includes("parquet") || normalized.includes("octet-stream");
|
|
67
|
+
}
|
|
68
|
+
export function buildDataCatalogOfferOperation(route, operation, apiBasePrefix = "/api") {
|
|
69
|
+
return {
|
|
70
|
+
...operation,
|
|
71
|
+
path: buildApiInteractionPath(route, apiBasePrefix),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function defineDataCatalogOfferSource(input) {
|
|
75
|
+
return input;
|
|
76
|
+
}
|
|
77
|
+
export function buildServiceApiInteractions(processName, definitions) {
|
|
78
|
+
return Object.fromEntries(Object.entries(definitions).map(([key, definition]) => [
|
|
79
|
+
key,
|
|
80
|
+
defineApiInteraction({
|
|
81
|
+
id: String(key),
|
|
82
|
+
topic: definition.topic ?? "system",
|
|
83
|
+
asset: definition.asset ?? "service",
|
|
84
|
+
objectType: definition.objectType ?? "runtime",
|
|
85
|
+
objectId: definition.objectId ?? processName,
|
|
86
|
+
attribute: definition.attribute,
|
|
87
|
+
method: definition.method ?? "GET",
|
|
88
|
+
routeOnly: definition.publishInUnsTree !== true,
|
|
89
|
+
registryTopic: "service-endpoints",
|
|
90
|
+
options: buildApiInteractionOptions(definition.method ?? "GET", definition.description, definition.tags, definition.queryParams, definition.requestBody, definition.publishInUnsTree !== true, "service-endpoints", buildServiceApiPayload(String(key), definition)),
|
|
91
|
+
handler: definition.handler,
|
|
92
|
+
}),
|
|
93
|
+
]));
|
|
94
|
+
}
|
|
95
|
+
export function buildApiInteractionsFromDataOfferSources(sources) {
|
|
96
|
+
return Object.fromEntries(Object.entries(sources).map(([key, source]) => [
|
|
97
|
+
key,
|
|
98
|
+
defineApiInteraction({
|
|
99
|
+
id: source.offerId,
|
|
100
|
+
topic: source.topic,
|
|
101
|
+
asset: source.asset,
|
|
102
|
+
objectType: source.objectType,
|
|
103
|
+
objectId: source.objectId,
|
|
104
|
+
attribute: source.attribute,
|
|
105
|
+
method: source.method ?? "GET",
|
|
106
|
+
routeOnly: true,
|
|
107
|
+
registryTopic: "data-offer-endpoints",
|
|
108
|
+
options: buildApiInteractionOptions(source.method ?? "GET", source.apiDescription ?? source.description, source.tags, source.queryParams, source.requestBody, true, "data-offer-endpoints"),
|
|
109
|
+
handler: source.handler,
|
|
110
|
+
}),
|
|
111
|
+
]));
|
|
112
|
+
}
|
|
113
|
+
export function buildDataCatalogOffersFromSources(sources) {
|
|
114
|
+
return Object.values(sources).map((source) => ({
|
|
115
|
+
offerId: source.offerId,
|
|
116
|
+
displayName: source.displayName,
|
|
117
|
+
description: source.description,
|
|
118
|
+
...(source.owner ? { owner: source.owner } : {}),
|
|
119
|
+
status: source.status ?? "available",
|
|
120
|
+
...(source.tags ? { tags: source.tags } : {}),
|
|
121
|
+
...(source.categories ? { categories: source.categories } : {}),
|
|
122
|
+
schemas: [source.schema],
|
|
123
|
+
operations: [
|
|
124
|
+
buildDataCatalogOfferOperation(source, {
|
|
125
|
+
id: `${source.offerId}-${(source.method ?? "GET").toLowerCase()}`,
|
|
126
|
+
method: source.method ?? "GET",
|
|
127
|
+
summary: source.displayName,
|
|
128
|
+
description: source.description,
|
|
129
|
+
...(source.tags ? { tags: source.tags } : {}),
|
|
130
|
+
...(source.queryParams ? { parameters: source.queryParams } : {}),
|
|
131
|
+
...(source.headers ? { headers: source.headers } : {}),
|
|
132
|
+
...(source.requestBody !== undefined ? { requestBody: source.requestBody } : {}),
|
|
133
|
+
responses: [
|
|
134
|
+
{
|
|
135
|
+
statusCode: source.response?.statusCode ?? "200",
|
|
136
|
+
...(source.response?.description ? { description: source.response.description } : { description: source.description }),
|
|
137
|
+
contentType: source.response?.contentType ?? source.schema.contentType ?? null,
|
|
138
|
+
schemas: [source.schema],
|
|
139
|
+
...(source.response?.examplePayloads ? { examplePayloads: source.response.examplePayloads } : {}),
|
|
140
|
+
...(source.response?.headers ? { headers: source.response.headers } : { headers: [] }),
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
}),
|
|
144
|
+
],
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
export function projectRowsForDataCatalogSchema(rows, schema) {
|
|
148
|
+
const leafFields = (schema.fields ?? []).filter((field) => field.type !== "array");
|
|
149
|
+
return rows.map((row) => {
|
|
150
|
+
const projected = {};
|
|
151
|
+
for (const field of leafFields) {
|
|
152
|
+
const outputKey = field.name;
|
|
153
|
+
const sourceKey = field.sourceKey?.trim() || field.name;
|
|
154
|
+
projected[outputKey] = getRowValue(row, sourceKey);
|
|
155
|
+
}
|
|
156
|
+
return projected;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
export function buildSchemaFieldPathIndex(schema) {
|
|
160
|
+
return (schema.fields ?? []).map((field) => ({
|
|
161
|
+
path: field.path ?? field.name,
|
|
162
|
+
name: field.name,
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
function inferSchemaRootType(contentType, fields) {
|
|
166
|
+
if (contentType.includes("parquet") || contentType.includes("octet-stream")) {
|
|
167
|
+
return "table";
|
|
168
|
+
}
|
|
169
|
+
if (fields.some((field) => (field.path ?? field.name).includes("[]"))) {
|
|
170
|
+
return "object";
|
|
171
|
+
}
|
|
172
|
+
return "object";
|
|
173
|
+
}
|
|
174
|
+
function buildSchemaFieldPath(name, type, fieldPathPrefix) {
|
|
175
|
+
if (fieldPathPrefix && type !== "array") {
|
|
176
|
+
return `${fieldPathPrefix}.${name}`;
|
|
177
|
+
}
|
|
178
|
+
return name;
|
|
179
|
+
}
|
|
180
|
+
function buildApiInteractionOptions(method, description, tags, queryParams, requestBody, routeOnly = true, registryTopic = "api-endpoints", serviceApi) {
|
|
181
|
+
if (method === "GET") {
|
|
182
|
+
return {
|
|
183
|
+
apiDescription: description,
|
|
184
|
+
...(routeOnly ? { routeOnly: true } : {}),
|
|
185
|
+
...(registryTopic ? { registryTopic } : {}),
|
|
186
|
+
...(serviceApi ? { serviceApi } : {}),
|
|
187
|
+
...(tags ? { tags } : {}),
|
|
188
|
+
...(queryParams
|
|
189
|
+
? {
|
|
190
|
+
queryParams: queryParams
|
|
191
|
+
.filter((parameter) => parameter.in === "query")
|
|
192
|
+
.map((parameter) => ({
|
|
193
|
+
name: parameter.name,
|
|
194
|
+
type: parameter.type === "number" || parameter.type === "boolean"
|
|
195
|
+
? parameter.type
|
|
196
|
+
: "string",
|
|
197
|
+
required: parameter.required,
|
|
198
|
+
...(parameter.description ? { description: parameter.description } : {}),
|
|
199
|
+
})),
|
|
200
|
+
}
|
|
201
|
+
: {}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const requestSchema = requestBody?.schemas?.[0]
|
|
205
|
+
? buildJsonSchemaFromCatalogSchema(requestBody.schemas[0])
|
|
206
|
+
: null;
|
|
207
|
+
return {
|
|
208
|
+
apiDescription: description,
|
|
209
|
+
...(routeOnly ? { routeOnly: true } : {}),
|
|
210
|
+
...(registryTopic ? { registryTopic } : {}),
|
|
211
|
+
...(serviceApi ? { serviceApi } : {}),
|
|
212
|
+
...(tags ? { tags } : {}),
|
|
213
|
+
...(requestBody
|
|
214
|
+
? {
|
|
215
|
+
requestBody: {
|
|
216
|
+
required: requestBody.required ?? true,
|
|
217
|
+
...(requestBody.description ? { description: requestBody.description } : {}),
|
|
218
|
+
...(requestSchema && typeof requestSchema === "object" ? { schema: requestSchema } : {}),
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
: {}),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function buildServiceApiPayload(id, definition) {
|
|
225
|
+
const schemas = definition.schema ? [definition.schema] : [];
|
|
226
|
+
const response = definition.response ?? {
|
|
227
|
+
statusCode: "200",
|
|
228
|
+
description: definition.description,
|
|
229
|
+
...(definition.schema?.contentType ? { contentType: definition.schema.contentType } : {}),
|
|
230
|
+
...(definition.schema ? { schemas: [definition.schema] } : {}),
|
|
231
|
+
};
|
|
232
|
+
return {
|
|
233
|
+
id,
|
|
234
|
+
summary: definition.description,
|
|
235
|
+
description: definition.description,
|
|
236
|
+
tags: definition.tags ?? [],
|
|
237
|
+
parameters: definition.queryParams ?? [],
|
|
238
|
+
headers: [],
|
|
239
|
+
requestBody: definition.requestBody ?? null,
|
|
240
|
+
responses: [response],
|
|
241
|
+
schemas,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function getRowValue(row, key) {
|
|
245
|
+
if (Object.prototype.hasOwnProperty.call(row, key)) {
|
|
246
|
+
return row[key];
|
|
247
|
+
}
|
|
248
|
+
const normalizedKey = key.toLowerCase();
|
|
249
|
+
const match = Object.keys(row).find((candidate) => candidate.toLowerCase() === normalizedKey);
|
|
250
|
+
return match ? row[match] : null;
|
|
251
|
+
}
|
|
252
|
+
function buildJsonSchemaFromCatalogSchema(schema) {
|
|
253
|
+
const leafFields = (schema.fields ?? []).filter((field) => field.type !== "array");
|
|
254
|
+
const properties = Object.fromEntries(leafFields.map((field) => [
|
|
255
|
+
field.name,
|
|
256
|
+
{
|
|
257
|
+
type: toJsonSchemaType(field.type ?? "string"),
|
|
258
|
+
...(field.description ? { description: field.description } : {}),
|
|
259
|
+
...(field.format ? { format: field.format } : {}),
|
|
260
|
+
...(field.example !== undefined ? { example: field.example } : {}),
|
|
261
|
+
...(Array.isArray(field.enumValues) && field.enumValues.length ? { enum: field.enumValues } : {}),
|
|
262
|
+
},
|
|
263
|
+
]));
|
|
264
|
+
const required = leafFields.filter((field) => field.required === true).map((field) => field.name);
|
|
265
|
+
return {
|
|
266
|
+
type: "object",
|
|
267
|
+
...(required.length ? { required } : {}),
|
|
268
|
+
properties,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function toJsonSchemaType(type) {
|
|
272
|
+
switch (type) {
|
|
273
|
+
case "number":
|
|
274
|
+
case "integer":
|
|
275
|
+
case "boolean":
|
|
276
|
+
case "array":
|
|
277
|
+
case "object":
|
|
278
|
+
return type;
|
|
279
|
+
default:
|
|
280
|
+
return "string";
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function buildExamplePayloadsFromFields(fields) {
|
|
284
|
+
const arrayFields = fields.filter((field) => field.type === "array");
|
|
285
|
+
const scalarFields = fields.filter((field) => field.type !== "array");
|
|
286
|
+
if (!scalarFields.length) {
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
if (arrayFields.length === 1 && isGenericListField(arrayFields[0].name)) {
|
|
290
|
+
const listField = arrayFields[0];
|
|
291
|
+
const rowFields = scalarFields.filter((field) => field.name !== "count");
|
|
292
|
+
if (!rowFields.length || rowFields.some((field) => field.example === undefined)) {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
const payload = {
|
|
296
|
+
[listField.name]: [Object.fromEntries(rowFields.map((field) => [field.name, field.example]))],
|
|
297
|
+
};
|
|
298
|
+
const countField = scalarFields.find((field) => field.name === "count");
|
|
299
|
+
if (countField) {
|
|
300
|
+
payload[countField.name] = countField.example ?? 1;
|
|
301
|
+
}
|
|
302
|
+
return [payload];
|
|
303
|
+
}
|
|
304
|
+
if (scalarFields.some((field) => field.example === undefined)) {
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
return [Object.fromEntries(scalarFields.map((field) => [field.name, field.example]))];
|
|
308
|
+
}
|
|
309
|
+
function isGenericListField(name) {
|
|
310
|
+
return name === "data" || name === "rows";
|
|
311
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DataCatalogSchemaRegistration } from "./api-interfaces.js";
|
|
2
|
+
export declare function writeSchemaRowsToParquet(input: {
|
|
3
|
+
rows: Array<Record<string, unknown>>;
|
|
4
|
+
schema: DataCatalogSchemaRegistration;
|
|
5
|
+
outputDir?: string;
|
|
6
|
+
fileName?: string;
|
|
7
|
+
}): Promise<string | null>;
|
|
8
|
+
export declare function buildParquetSchemaFromCatalogSchema(schema: DataCatalogSchemaRegistration): any;
|
package/dist/parquet.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { ParquetSchema, ParquetWriter } from "parquets";
|
|
6
|
+
import logger from "@uns-kit/core/logger.js";
|
|
7
|
+
const PARQUET_TYPE_MAP = {
|
|
8
|
+
string: "UTF8",
|
|
9
|
+
number: "DOUBLE",
|
|
10
|
+
integer: "INT64",
|
|
11
|
+
boolean: "BOOLEAN",
|
|
12
|
+
date: "TIMESTAMP_MILLIS",
|
|
13
|
+
"date-time": "TIMESTAMP_MILLIS",
|
|
14
|
+
};
|
|
15
|
+
export async function writeSchemaRowsToParquet(input) {
|
|
16
|
+
try {
|
|
17
|
+
const outputDir = input.outputDir ?? path.join(os.tmpdir(), "uns-data-offers");
|
|
18
|
+
const fileName = input.fileName ?? `${randomUUID()}.parquet`;
|
|
19
|
+
const filePath = path.join(outputDir, fileName);
|
|
20
|
+
if (!fs.existsSync(outputDir)) {
|
|
21
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
const writer = await ParquetWriter.openFile(buildParquetSchemaFromCatalogSchema(input.schema), filePath);
|
|
24
|
+
for (const row of input.rows) {
|
|
25
|
+
await writer.appendRow(normalizeParquetRow(row));
|
|
26
|
+
}
|
|
27
|
+
await writer.close();
|
|
28
|
+
return filePath;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
logger.error("Failed to write schema-driven parquet:", error);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function buildParquetSchemaFromCatalogSchema(schema) {
|
|
36
|
+
const schemaDef = {};
|
|
37
|
+
for (const field of schema.fields ?? []) {
|
|
38
|
+
const parquetType = toParquetType(field.type ?? "string", field.format ?? null);
|
|
39
|
+
if (!parquetType) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
schemaDef[field.name] = {
|
|
43
|
+
type: parquetType,
|
|
44
|
+
optional: field.required !== true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return new ParquetSchema(schemaDef);
|
|
48
|
+
}
|
|
49
|
+
function toParquetType(type, format) {
|
|
50
|
+
if (format && PARQUET_TYPE_MAP[format]) {
|
|
51
|
+
return PARQUET_TYPE_MAP[format];
|
|
52
|
+
}
|
|
53
|
+
return PARQUET_TYPE_MAP[type] ?? null;
|
|
54
|
+
}
|
|
55
|
+
function normalizeParquetRow(row) {
|
|
56
|
+
const normalized = {};
|
|
57
|
+
for (const [key, value] of Object.entries(row)) {
|
|
58
|
+
normalized[key] = value instanceof Date ? new Date(value) : value;
|
|
59
|
+
}
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
package/dist/uns-api-plugin.js
CHANGED
|
@@ -22,9 +22,18 @@ const unsApiPlugin = ({ define }) => {
|
|
|
22
22
|
unsApiProxy.event.on("unsProxyProducedApiEndpoints", (event) => {
|
|
23
23
|
internals.processMqttProxy.publish(event.statusTopic, JSON.stringify(event.producedApiEndpoints));
|
|
24
24
|
});
|
|
25
|
+
unsApiProxy.event.on("unsProxyProducedServiceEndpoints", (event) => {
|
|
26
|
+
internals.processMqttProxy.publish(event.statusTopic, JSON.stringify(event.producedServiceEndpoints));
|
|
27
|
+
});
|
|
28
|
+
unsApiProxy.event.on("unsProxyProducedDataOfferEndpoints", (event) => {
|
|
29
|
+
internals.processMqttProxy.publish(event.statusTopic, JSON.stringify(event.producedDataOfferEndpoints));
|
|
30
|
+
});
|
|
25
31
|
unsApiProxy.event.on("unsProxyProducedApiCatchAll", (event) => {
|
|
26
32
|
internals.processMqttProxy.publish(event.statusTopic, JSON.stringify(event.producedCatchall));
|
|
27
33
|
});
|
|
34
|
+
unsApiProxy.event.on("unsProxyProducedDataCatalogOffers", (event) => {
|
|
35
|
+
internals.processMqttProxy.publish(event.statusTopic, JSON.stringify(event.producedDataCatalogOffers));
|
|
36
|
+
});
|
|
28
37
|
unsApiProxy.event.on("mqttProxyStatus", (event) => {
|
|
29
38
|
const time = UnsPacket.formatToISO8601(new Date());
|
|
30
39
|
const unsMessage = { data: { time, value: event.value, uom: event.uom } };
|
package/dist/uns-api-proxy.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { UnsTopics } from "@uns-kit/core/uns/uns-topics.js";
|
|
|
4
4
|
import { IApiProxyOptions, IGetEndpointOptions, IPostEndpointOptions } from "@uns-kit/core/uns/uns-interfaces.js";
|
|
5
5
|
import { UnsAsset } from "@uns-kit/core/uns/uns-asset.js";
|
|
6
6
|
import { UnsObjectType, UnsObjectId } from "@uns-kit/core/uns/uns-object.js";
|
|
7
|
+
import type { DataCatalogOfferRegistration } from "./api-interfaces.js";
|
|
7
8
|
export default class UnsApiProxy extends UnsProxy {
|
|
8
9
|
instanceName: string;
|
|
9
10
|
private topicBuilder;
|
|
@@ -18,7 +19,9 @@ export default class UnsApiProxy extends UnsProxy {
|
|
|
18
19
|
private startedAt;
|
|
19
20
|
private statusInterval;
|
|
20
21
|
private readonly statusIntervalMs;
|
|
22
|
+
private readonly dataCatalogOffers;
|
|
21
23
|
constructor(processName: string, instanceName: string, options: IApiProxyOptions);
|
|
24
|
+
getProcessName(): string;
|
|
22
25
|
/**
|
|
23
26
|
* Unregister endpoint
|
|
24
27
|
* @param topic - The API topic
|
|
@@ -51,20 +54,27 @@ export default class UnsApiProxy extends UnsProxy {
|
|
|
51
54
|
tags?: string[];
|
|
52
55
|
queryParams?: IGetEndpointOptions["queryParams"];
|
|
53
56
|
}): Promise<void>;
|
|
54
|
-
/**
|
|
55
|
-
* Register a POST endpoint.
|
|
56
|
-
* @param topic - The API topic
|
|
57
|
-
* @param attribute - The attribute for the topic.
|
|
58
|
-
* @param options.apiDescription - Optional description.
|
|
59
|
-
* @param options.tags - Optional tags.
|
|
60
|
-
* @param options.requestBody - Optional request body schema for Swagger.
|
|
61
|
-
*/
|
|
62
57
|
post(topic: UnsTopics, asset: UnsAsset, objectType: UnsObjectType, objectId: UnsObjectId, attribute: UnsAttribute, options?: IPostEndpointOptions): Promise<void>;
|
|
58
|
+
put(topic: UnsTopics, asset: UnsAsset, objectType: UnsObjectType, objectId: UnsObjectId, attribute: UnsAttribute, options?: IPostEndpointOptions): Promise<void>;
|
|
59
|
+
patch(topic: UnsTopics, asset: UnsAsset, objectType: UnsObjectType, objectId: UnsObjectId, attribute: UnsAttribute, options?: IPostEndpointOptions): Promise<void>;
|
|
60
|
+
delete(topic: UnsTopics, asset: UnsAsset, objectType: UnsObjectType, objectId: UnsObjectId, attribute: UnsAttribute, options?: IPostEndpointOptions): Promise<void>;
|
|
61
|
+
private registerMutationEndpoint;
|
|
62
|
+
registerDataOffer(input: DataCatalogOfferRegistration): void;
|
|
63
63
|
private emitStatusMetrics;
|
|
64
64
|
private registerHealthEndpoint;
|
|
65
|
+
private getApiEventName;
|
|
65
66
|
private extractBearerToken;
|
|
66
67
|
private getPublicKeyFromJwks;
|
|
67
68
|
private fetchJwksKeys;
|
|
68
69
|
private certFromX5c;
|
|
69
70
|
stop(): Promise<void>;
|
|
71
|
+
private publishDataCatalogOffer;
|
|
72
|
+
private emitDataCatalogOffers;
|
|
73
|
+
private normalizeDataOfferOperation;
|
|
74
|
+
private normalizeDataOfferParameter;
|
|
75
|
+
private normalizeDataOfferRequestBody;
|
|
76
|
+
private normalizeDataOfferResponse;
|
|
77
|
+
private normalizeDataOfferSchema;
|
|
78
|
+
private normalizeDataOfferSchemaField;
|
|
79
|
+
private resolveOfferSchemas;
|
|
70
80
|
}
|
package/dist/uns-api-proxy.js
CHANGED
|
@@ -31,6 +31,21 @@ const buildSwaggerPath = (base, processName, instanceName) => {
|
|
|
31
31
|
}
|
|
32
32
|
return `${baseWithProcess}/${instanceName}/swagger.json`.replace(/\/{2,}/g, "/");
|
|
33
33
|
};
|
|
34
|
+
const normalizeStringArray = (value) => Array.isArray(value)
|
|
35
|
+
? Array.from(new Set(value
|
|
36
|
+
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
|
|
37
|
+
.filter((entry) => entry.length > 0)))
|
|
38
|
+
: [];
|
|
39
|
+
const normalizePathValue = (value) => {
|
|
40
|
+
if (typeof value !== "string") {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const trimmed = value.trim();
|
|
44
|
+
if (!trimmed) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
48
|
+
};
|
|
34
49
|
export default class UnsApiProxy extends UnsProxy {
|
|
35
50
|
instanceName;
|
|
36
51
|
topicBuilder;
|
|
@@ -45,6 +60,7 @@ export default class UnsApiProxy extends UnsProxy {
|
|
|
45
60
|
startedAt;
|
|
46
61
|
statusInterval = null;
|
|
47
62
|
statusIntervalMs = 10_000;
|
|
63
|
+
dataCatalogOffers = new Map();
|
|
48
64
|
constructor(processName, instanceName, options) {
|
|
49
65
|
super();
|
|
50
66
|
this.options = options;
|
|
@@ -76,6 +92,9 @@ export default class UnsApiProxy extends UnsProxy {
|
|
|
76
92
|
setTimeout(() => this.emitStatusMetrics(), 0);
|
|
77
93
|
this.statusInterval = setInterval(() => this.emitStatusMetrics(), this.statusIntervalMs);
|
|
78
94
|
}
|
|
95
|
+
getProcessName() {
|
|
96
|
+
return this.processName;
|
|
97
|
+
}
|
|
79
98
|
/**
|
|
80
99
|
* Unregister endpoint
|
|
81
100
|
* @param topic - The API topic
|
|
@@ -138,11 +157,15 @@ export default class UnsApiProxy extends UnsProxy {
|
|
|
138
157
|
timestamp: time,
|
|
139
158
|
topic: topic,
|
|
140
159
|
attribute: attribute,
|
|
160
|
+
routeOnly: options?.routeOnly === true,
|
|
161
|
+
registryTopic: options?.registryTopic ??
|
|
162
|
+
"api-endpoints",
|
|
141
163
|
apiHost: `http://${ip}:${port}`,
|
|
142
164
|
apiEndpoint: apiPath,
|
|
143
165
|
apiMethod: "GET",
|
|
144
166
|
apiQueryParams: options.queryParams,
|
|
145
167
|
apiDescription: options?.apiDescription,
|
|
168
|
+
serviceApi: options?.serviceApi ?? null,
|
|
146
169
|
attributeType: UnsAttributeType.Api,
|
|
147
170
|
apiSwaggerEndpoint: swaggerPath,
|
|
148
171
|
asset,
|
|
@@ -383,15 +406,19 @@ export default class UnsApiProxy extends UnsProxy {
|
|
|
383
406
|
swaggerPath,
|
|
384
407
|
});
|
|
385
408
|
}
|
|
386
|
-
/**
|
|
387
|
-
* Register a POST endpoint.
|
|
388
|
-
* @param topic - The API topic
|
|
389
|
-
* @param attribute - The attribute for the topic.
|
|
390
|
-
* @param options.apiDescription - Optional description.
|
|
391
|
-
* @param options.tags - Optional tags.
|
|
392
|
-
* @param options.requestBody - Optional request body schema for Swagger.
|
|
393
|
-
*/
|
|
394
409
|
async post(topic, asset, objectType, objectId, attribute, options) {
|
|
410
|
+
await this.registerMutationEndpoint("POST", topic, asset, objectType, objectId, attribute, options);
|
|
411
|
+
}
|
|
412
|
+
async put(topic, asset, objectType, objectId, attribute, options) {
|
|
413
|
+
await this.registerMutationEndpoint("PUT", topic, asset, objectType, objectId, attribute, options);
|
|
414
|
+
}
|
|
415
|
+
async patch(topic, asset, objectType, objectId, attribute, options) {
|
|
416
|
+
await this.registerMutationEndpoint("PATCH", topic, asset, objectType, objectId, attribute, options);
|
|
417
|
+
}
|
|
418
|
+
async delete(topic, asset, objectType, objectId, attribute, options) {
|
|
419
|
+
await this.registerMutationEndpoint("DELETE", topic, asset, objectType, objectId, attribute, options);
|
|
420
|
+
}
|
|
421
|
+
async registerMutationEndpoint(method, topic, asset, objectType, objectId, attribute, options) {
|
|
395
422
|
while (this.app.server.listening === false) {
|
|
396
423
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
397
424
|
}
|
|
@@ -399,6 +426,7 @@ export default class UnsApiProxy extends UnsProxy {
|
|
|
399
426
|
const fullPath = buildUnsRoutePath(topic, asset, objectType, objectId, attribute);
|
|
400
427
|
const apiPath = `${this.apiBasePrefix}${fullPath}`.replace(/\/{2,}/g, "/");
|
|
401
428
|
const swaggerPath = buildSwaggerPath(this.swaggerBasePrefix, this.processName, this.instanceName);
|
|
429
|
+
const methodKey = method.toLowerCase();
|
|
402
430
|
try {
|
|
403
431
|
const addressInfo = this.app.server.address();
|
|
404
432
|
let ip;
|
|
@@ -415,11 +443,15 @@ export default class UnsApiProxy extends UnsProxy {
|
|
|
415
443
|
timestamp: time,
|
|
416
444
|
topic,
|
|
417
445
|
attribute,
|
|
446
|
+
routeOnly: options?.routeOnly === true,
|
|
447
|
+
registryTopic: options?.registryTopic ??
|
|
448
|
+
"api-endpoints",
|
|
418
449
|
apiHost: `http://${ip}:${port}`,
|
|
419
450
|
apiEndpoint: apiPath,
|
|
420
|
-
apiMethod:
|
|
451
|
+
apiMethod: method,
|
|
421
452
|
apiQueryParams: [],
|
|
422
453
|
apiDescription: options?.apiDescription,
|
|
454
|
+
serviceApi: options?.serviceApi ?? null,
|
|
423
455
|
attributeType: UnsAttributeType.Api,
|
|
424
456
|
apiSwaggerEndpoint: swaggerPath,
|
|
425
457
|
asset,
|
|
@@ -427,10 +459,11 @@ export default class UnsApiProxy extends UnsProxy {
|
|
|
427
459
|
objectId,
|
|
428
460
|
});
|
|
429
461
|
const handler = (req, res) => {
|
|
430
|
-
this.event.emit(
|
|
462
|
+
this.event.emit(this.getApiEventName(method), { req, res });
|
|
431
463
|
};
|
|
464
|
+
const routerMethod = this.app.router[methodKey].bind(this.app.router);
|
|
432
465
|
if (this.options?.jwks?.wellKnownJwksUrl) {
|
|
433
|
-
|
|
466
|
+
routerMethod(fullPath, async (req, res) => {
|
|
434
467
|
try {
|
|
435
468
|
const token = this.extractBearerToken(req, res);
|
|
436
469
|
if (!token)
|
|
@@ -451,13 +484,13 @@ export default class UnsApiProxy extends UnsProxy {
|
|
|
451
484
|
}
|
|
452
485
|
handler(req, res);
|
|
453
486
|
}
|
|
454
|
-
catch (
|
|
487
|
+
catch (_err) {
|
|
455
488
|
return res.status(401).json({ error: "Invalid token" });
|
|
456
489
|
}
|
|
457
490
|
});
|
|
458
491
|
}
|
|
459
492
|
else if (this.options?.jwtSecret) {
|
|
460
|
-
|
|
493
|
+
routerMethod(fullPath, (req, res) => {
|
|
461
494
|
const authHeader = req.headers["authorization"];
|
|
462
495
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
463
496
|
return res.status(401).json({ error: "Missing or invalid Authorization header" });
|
|
@@ -478,19 +511,19 @@ export default class UnsApiProxy extends UnsProxy {
|
|
|
478
511
|
}
|
|
479
512
|
handler(req, res);
|
|
480
513
|
}
|
|
481
|
-
catch (
|
|
514
|
+
catch (_err) {
|
|
482
515
|
return res.status(401).json({ error: "Invalid token" });
|
|
483
516
|
}
|
|
484
517
|
});
|
|
485
518
|
}
|
|
486
519
|
else {
|
|
487
|
-
|
|
520
|
+
routerMethod(fullPath, handler);
|
|
488
521
|
}
|
|
489
522
|
if (this.app.swaggerSpec) {
|
|
490
523
|
const requestBody = options?.requestBody;
|
|
491
524
|
this.app.swaggerSpec.paths = this.app.swaggerSpec.paths || {};
|
|
492
525
|
this.app.swaggerSpec.paths[apiPath] = this.app.swaggerSpec.paths[apiPath] || {};
|
|
493
|
-
this.app.swaggerSpec.paths[apiPath]
|
|
526
|
+
this.app.swaggerSpec.paths[apiPath][methodKey] = {
|
|
494
527
|
summary: options?.apiDescription || "No description",
|
|
495
528
|
tags: options?.tags || [],
|
|
496
529
|
...(requestBody
|
|
@@ -516,9 +549,58 @@ export default class UnsApiProxy extends UnsProxy {
|
|
|
516
549
|
}
|
|
517
550
|
}
|
|
518
551
|
catch (error) {
|
|
519
|
-
logger.error(`${this.instanceNameWithSuffix} - Error registering
|
|
552
|
+
logger.error(`${this.instanceNameWithSuffix} - Error registering ${method} route ${fullPath}: ${error.message}`);
|
|
520
553
|
}
|
|
521
554
|
}
|
|
555
|
+
registerDataOffer(input) {
|
|
556
|
+
const offerId = typeof input.offerId === "string" ? input.offerId.trim() : "";
|
|
557
|
+
const displayName = typeof input.displayName === "string" ? input.displayName.trim() : "";
|
|
558
|
+
if (!offerId || !displayName) {
|
|
559
|
+
throw new Error("Data catalog offer requires non-empty offerId and displayName.");
|
|
560
|
+
}
|
|
561
|
+
const offerSchemas = (Array.isArray(input.schemas) ? input.schemas : []).map((schema) => this.normalizeDataOfferSchema(schema));
|
|
562
|
+
const offerSchemaIndex = new Map(offerSchemas.map((schema) => [schema.id, schema]));
|
|
563
|
+
const normalizedOperations = (Array.isArray(input.operations) ? input.operations : [])
|
|
564
|
+
.map((operation, index) => this.normalizeDataOfferOperation(operation, offerId, index, offerSchemaIndex))
|
|
565
|
+
.filter((operation) => Boolean(operation));
|
|
566
|
+
if (!normalizedOperations.length) {
|
|
567
|
+
throw new Error(`Data catalog offer ${offerId} requires at least one operation.`);
|
|
568
|
+
}
|
|
569
|
+
const basePaths = Array.from(new Set([
|
|
570
|
+
...(Array.isArray(input.basePaths) ? input.basePaths.map((entry) => normalizePathValue(entry)) : []),
|
|
571
|
+
...normalizedOperations.map((operation) => {
|
|
572
|
+
const segments = operation.path.split("/").filter(Boolean);
|
|
573
|
+
if (segments.length <= 1) {
|
|
574
|
+
return operation.path;
|
|
575
|
+
}
|
|
576
|
+
return `/${segments.slice(0, -1).join("/")}`;
|
|
577
|
+
}),
|
|
578
|
+
].filter((value) => Boolean(value))));
|
|
579
|
+
this.publishDataCatalogOffer({
|
|
580
|
+
offerId,
|
|
581
|
+
displayName,
|
|
582
|
+
description: typeof input.description === "string" ? input.description.trim() || null : null,
|
|
583
|
+
owner: typeof input.owner === "string" ? input.owner.trim() || null : null,
|
|
584
|
+
status: typeof input.status === "string" && input.status.trim() ? input.status.trim() : "available",
|
|
585
|
+
tags: normalizeStringArray(input.tags),
|
|
586
|
+
categories: normalizeStringArray(input.categories),
|
|
587
|
+
microserviceName: typeof input.microserviceName === "string" && input.microserviceName.trim()
|
|
588
|
+
? input.microserviceName.trim()
|
|
589
|
+
: this.processName,
|
|
590
|
+
version: typeof input.version === "string" && input.version.trim() ? input.version.trim() : packageJson.version,
|
|
591
|
+
swaggerPath: normalizePathValue(input.swaggerPath),
|
|
592
|
+
basePaths,
|
|
593
|
+
operations: normalizedOperations,
|
|
594
|
+
schemas: offerSchemas,
|
|
595
|
+
metadata: input.metadata ?? null,
|
|
596
|
+
packageName: packageJson.name,
|
|
597
|
+
processName: this.processName,
|
|
598
|
+
processVersion: packageJson.version,
|
|
599
|
+
instanceName: this.instanceName,
|
|
600
|
+
controllerName: process.env["UNS_CONTROLLER_NAME"] ?? null,
|
|
601
|
+
controllerPublicBase: process.env["UNS_CONTROLLER_PUBLIC_BASE"] ?? process.env["UNS_PUBLIC_BASE"] ?? null,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
522
604
|
emitStatusMetrics() {
|
|
523
605
|
const uptimeMinutes = Math.round((Date.now() - this.startedAt) / 60000);
|
|
524
606
|
// Process-level status
|
|
@@ -547,6 +629,7 @@ export default class UnsApiProxy extends UnsProxy {
|
|
|
547
629
|
uom: DataSizeMeasurements.Bit,
|
|
548
630
|
statusTopic: this.instanceStatusTopic + "alive",
|
|
549
631
|
});
|
|
632
|
+
this.emitDataCatalogOffers();
|
|
550
633
|
}
|
|
551
634
|
registerHealthEndpoint() {
|
|
552
635
|
const routePath = "/status";
|
|
@@ -575,6 +658,20 @@ export default class UnsApiProxy extends UnsProxy {
|
|
|
575
658
|
};
|
|
576
659
|
}
|
|
577
660
|
}
|
|
661
|
+
getApiEventName(method) {
|
|
662
|
+
switch (method) {
|
|
663
|
+
case "GET":
|
|
664
|
+
return "apiGetEvent";
|
|
665
|
+
case "POST":
|
|
666
|
+
return "apiPostEvent";
|
|
667
|
+
case "PUT":
|
|
668
|
+
return "apiPutEvent";
|
|
669
|
+
case "PATCH":
|
|
670
|
+
return "apiPatchEvent";
|
|
671
|
+
case "DELETE":
|
|
672
|
+
return "apiDeleteEvent";
|
|
673
|
+
}
|
|
674
|
+
}
|
|
578
675
|
extractBearerToken(req, res) {
|
|
579
676
|
const authHeader = req.headers["authorization"];
|
|
580
677
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
@@ -647,4 +744,117 @@ export default class UnsApiProxy extends UnsProxy {
|
|
|
647
744
|
}
|
|
648
745
|
await super.stop();
|
|
649
746
|
}
|
|
747
|
+
publishDataCatalogOffer(offer) {
|
|
748
|
+
const offerId = typeof offer.offerId === "string" ? offer.offerId.trim() : "";
|
|
749
|
+
if (!offerId) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
this.dataCatalogOffers.set(offerId, offer);
|
|
753
|
+
this.emitDataCatalogOffers();
|
|
754
|
+
logger.info(`${this.instanceNameWithSuffix} - Registered data catalog offer: ${offerId}`);
|
|
755
|
+
}
|
|
756
|
+
emitDataCatalogOffers() {
|
|
757
|
+
if (this.instanceStatusTopic === "" || this.dataCatalogOffers.size === 0) {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
this.event.emit("unsProxyProducedDataCatalogOffers", {
|
|
761
|
+
producedDataCatalogOffers: [...this.dataCatalogOffers.values()],
|
|
762
|
+
statusTopic: this.instanceStatusTopic + "data-catalog-offers",
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
normalizeDataOfferOperation(operation, offerId, index, offerSchemaIndex) {
|
|
766
|
+
const method = typeof operation.method === "string" ? operation.method.trim().toUpperCase() : "";
|
|
767
|
+
const path = normalizePathValue(operation.path);
|
|
768
|
+
if (!method || !path) {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
return {
|
|
772
|
+
id: typeof operation.id === "string" && operation.id.trim() ? operation.id.trim() : `${offerId}-${method.toLowerCase()}-${index + 1}`,
|
|
773
|
+
method,
|
|
774
|
+
path,
|
|
775
|
+
summary: typeof operation.summary === "string" ? operation.summary.trim() || null : null,
|
|
776
|
+
description: typeof operation.description === "string" ? operation.description.trim() || null : null,
|
|
777
|
+
tags: normalizeStringArray(operation.tags),
|
|
778
|
+
deprecated: operation.deprecated === true,
|
|
779
|
+
parameters: (Array.isArray(operation.parameters) ? operation.parameters : []).map((parameter) => this.normalizeDataOfferParameter(parameter)),
|
|
780
|
+
headers: (Array.isArray(operation.headers) ? operation.headers : []).map((parameter) => this.normalizeDataOfferParameter(parameter)),
|
|
781
|
+
requestBody: operation.requestBody ? this.normalizeDataOfferRequestBody(operation.requestBody, offerSchemaIndex) : null,
|
|
782
|
+
responses: (Array.isArray(operation.responses) ? operation.responses : []).map((response) => this.normalizeDataOfferResponse(response, offerSchemaIndex)),
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
normalizeDataOfferParameter(parameter) {
|
|
786
|
+
return {
|
|
787
|
+
name: typeof parameter.name === "string" ? parameter.name.trim() : "",
|
|
788
|
+
in: typeof parameter.in === "string" ? parameter.in.trim() : "query",
|
|
789
|
+
required: parameter.required === true,
|
|
790
|
+
description: typeof parameter.description === "string" ? parameter.description.trim() || null : null,
|
|
791
|
+
type: typeof parameter.type === "string" && parameter.type.trim() ? parameter.type.trim() : "string",
|
|
792
|
+
format: typeof parameter.format === "string" ? parameter.format.trim() || null : null,
|
|
793
|
+
nullable: parameter.nullable === true,
|
|
794
|
+
example: parameter.example ?? null,
|
|
795
|
+
enumValues: normalizeStringArray(parameter.enumValues),
|
|
796
|
+
schema: parameter.schema ?? null,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
normalizeDataOfferRequestBody(requestBody, offerSchemaIndex) {
|
|
800
|
+
return {
|
|
801
|
+
required: requestBody.required === true,
|
|
802
|
+
description: typeof requestBody.description === "string" ? requestBody.description.trim() || null : null,
|
|
803
|
+
contentType: typeof requestBody.contentType === "string" ? requestBody.contentType.trim() || null : null,
|
|
804
|
+
schemas: this.resolveOfferSchemas(requestBody.schemas, requestBody.schemaIds, offerSchemaIndex),
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
normalizeDataOfferResponse(response, offerSchemaIndex) {
|
|
808
|
+
return {
|
|
809
|
+
statusCode: typeof response.statusCode === "string" ? response.statusCode.trim() : "",
|
|
810
|
+
description: typeof response.description === "string" ? response.description.trim() || null : null,
|
|
811
|
+
contentType: typeof response.contentType === "string" ? response.contentType.trim() || null : null,
|
|
812
|
+
schemas: this.resolveOfferSchemas(response.schemas, response.schemaIds, offerSchemaIndex),
|
|
813
|
+
examplePayloads: Array.isArray(response.examplePayloads) ? response.examplePayloads : [],
|
|
814
|
+
headers: (Array.isArray(response.headers) ? response.headers : []).map((parameter) => this.normalizeDataOfferParameter(parameter)),
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
normalizeDataOfferSchema(schema) {
|
|
818
|
+
return {
|
|
819
|
+
id: typeof schema.id === "string" ? schema.id.trim() : "",
|
|
820
|
+
title: typeof schema.title === "string" ? schema.title.trim() : "",
|
|
821
|
+
kind: typeof schema.kind === "string" && schema.kind.trim() ? schema.kind.trim() : "schema",
|
|
822
|
+
source: typeof schema.source === "string" && schema.source.trim() ? schema.source.trim() : "registered",
|
|
823
|
+
contentType: typeof schema.contentType === "string" ? schema.contentType.trim() || null : null,
|
|
824
|
+
rootType: typeof schema.rootType === "string" && schema.rootType.trim() ? schema.rootType.trim() : "object",
|
|
825
|
+
nullable: schema.nullable === true,
|
|
826
|
+
description: typeof schema.description === "string" ? schema.description.trim() || null : null,
|
|
827
|
+
fields: (Array.isArray(schema.fields) ? schema.fields : []).map((field) => this.normalizeDataOfferSchemaField(field)),
|
|
828
|
+
examplePayloads: Array.isArray(schema.examplePayloads) ? schema.examplePayloads : [],
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
normalizeDataOfferSchemaField(field) {
|
|
832
|
+
const defaultName = typeof field.name === "string" ? field.name.trim() : "";
|
|
833
|
+
const path = typeof field.path === "string" && field.path.trim() ? field.path.trim() : defaultName;
|
|
834
|
+
return {
|
|
835
|
+
path,
|
|
836
|
+
name: defaultName || (path.split(".").at(-1) ?? path),
|
|
837
|
+
type: typeof field.type === "string" && field.type.trim() ? field.type.trim() : "string",
|
|
838
|
+
format: typeof field.format === "string" ? field.format.trim() || null : null,
|
|
839
|
+
nullable: field.nullable === true,
|
|
840
|
+
required: field.required === true,
|
|
841
|
+
description: typeof field.description === "string" ? field.description.trim() || null : null,
|
|
842
|
+
enumValues: normalizeStringArray(field.enumValues),
|
|
843
|
+
example: field.example ?? null,
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
resolveOfferSchemas(inlineSchemas, schemaIds, offerSchemaIndex) {
|
|
847
|
+
const result = (Array.isArray(inlineSchemas) ? inlineSchemas : []).map((schema) => this.normalizeDataOfferSchema(schema));
|
|
848
|
+
for (const schemaId of Array.isArray(schemaIds) ? schemaIds : []) {
|
|
849
|
+
const normalizedId = typeof schemaId === "string" ? schemaId.trim() : "";
|
|
850
|
+
if (!normalizedId) {
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
const resolved = offerSchemaIndex.get(normalizedId);
|
|
854
|
+
if (resolved && !result.some((schema) => schema.id === resolved.id)) {
|
|
855
|
+
result.push(resolved);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return result;
|
|
859
|
+
}
|
|
650
860
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uns-kit/api",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.46",
|
|
4
4
|
"description": "Express-powered API gateway plugin for UnsProxyProcess with JWT/JWKS support.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,16 +31,18 @@
|
|
|
31
31
|
"main": "dist/index.js",
|
|
32
32
|
"types": "dist/index.d.ts",
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"jsonwebtoken": "^9.0.2",
|
|
35
34
|
"cookie-parser": "^1.4.7",
|
|
36
35
|
"express": "^5.1.0",
|
|
36
|
+
"jsonwebtoken": "^9.0.2",
|
|
37
37
|
"multer": "^2.0.2",
|
|
38
|
-
"
|
|
38
|
+
"parquets": "^0.10.10",
|
|
39
|
+
"uuid": "^14.0.0",
|
|
40
|
+
"@uns-kit/core": "2.0.46"
|
|
39
41
|
},
|
|
40
42
|
"devDependencies": {
|
|
41
|
-
"@types/jsonwebtoken": "^9.0.10",
|
|
42
43
|
"@types/cookie-parser": "^1.4.9",
|
|
43
44
|
"@types/express": "^5.0.3",
|
|
45
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
44
46
|
"@types/multer": "^2.0.0"
|
|
45
47
|
},
|
|
46
48
|
"scripts": {
|