@objectstack/rest 7.8.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +248 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -1
- package/dist/index.d.ts +33 -1
- package/dist/index.js +248 -11
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -37,6 +37,9 @@ __export(index_exports, {
|
|
|
37
37
|
});
|
|
38
38
|
module.exports = __toCommonJS(index_exports);
|
|
39
39
|
|
|
40
|
+
// src/rest-server.ts
|
|
41
|
+
var import_core = require("@objectstack/core");
|
|
42
|
+
|
|
40
43
|
// src/route-manager.ts
|
|
41
44
|
var RouteManager = class {
|
|
42
45
|
constructor(server) {
|
|
@@ -308,6 +311,32 @@ function mapDataError(error, object) {
|
|
|
308
311
|
}
|
|
309
312
|
};
|
|
310
313
|
}
|
|
314
|
+
const unknownColumn = /has no column named\s+["'`]?([a-z0-9_]+)/i.exec(raw) || /no such column:\s*["'`]?([a-z0-9_.]+)/i.exec(raw) || /unknown column\s+["'`]([a-z0-9_]+)["'`]/i.exec(raw) || /column\s+["'`]([a-z0-9_]+)["'`]\s+of relation\s+\S+\s+does not exist/i.exec(raw);
|
|
315
|
+
if (unknownColumn) {
|
|
316
|
+
const field = unknownColumn[1]?.split(".").pop();
|
|
317
|
+
return {
|
|
318
|
+
status: 400,
|
|
319
|
+
body: {
|
|
320
|
+
error: field ? `Unknown field '${field}'${object ? ` on object '${object}'` : ""}` : "Request references a field that does not exist",
|
|
321
|
+
code: "INVALID_FIELD",
|
|
322
|
+
...field ? { field } : {},
|
|
323
|
+
...object ? { object } : {}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const notNull = /not null constraint failed:\s*\S*?\.([a-z0-9_]+)/i.exec(raw) || /null value in column\s+["'`]([a-z0-9_]+)["'`]/i.exec(raw) || /column\s+["'`]([a-z0-9_]+)["'`]\s+cannot be null/i.exec(raw);
|
|
328
|
+
if (notNull) {
|
|
329
|
+
const field = notNull[1];
|
|
330
|
+
return {
|
|
331
|
+
status: 400,
|
|
332
|
+
body: {
|
|
333
|
+
error: `${field} is required`,
|
|
334
|
+
code: "VALIDATION_FAILED",
|
|
335
|
+
fields: [{ field, code: "required", message: `${field} is required` }],
|
|
336
|
+
...object ? { object } : {}
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
311
340
|
const looksLikeUnknownObject = lower.includes("no such table") || lower.includes("relation") && lower.includes("does not exist") || lower.includes("table not found") || lower.includes("unknown object") || lower.includes("object not found") || lower.includes("no driver available") || object !== void 0 && lower.includes(`'${object.toLowerCase()}'`) && lower.includes("not");
|
|
312
341
|
if (looksLikeUnknownObject) {
|
|
313
342
|
return {
|
|
@@ -445,7 +474,16 @@ function rowsToCsv(fields, rows, includeHeader) {
|
|
|
445
474
|
return lines.join("\r\n") + (lines.length > 0 ? "\r\n" : "");
|
|
446
475
|
}
|
|
447
476
|
var RestServer = class {
|
|
448
|
-
constructor(server, protocol, config = {}, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider) {
|
|
477
|
+
constructor(server, protocol, config = {}, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider) {
|
|
478
|
+
/**
|
|
479
|
+
* Short-TTL cache for `hostname → environmentId` (P1-4). `resolveByHostname`
|
|
480
|
+
* is a control-plane lookup (typically a DB query) that otherwise runs on
|
|
481
|
+
* *every* unscoped request; caching it — including negative results, so
|
|
482
|
+
* unknown hosts don't hammer the registry — removes that per-request cost.
|
|
483
|
+
* The TTL is short so a newly-bound hostname becomes routable quickly.
|
|
484
|
+
*/
|
|
485
|
+
this.hostnameCache = /* @__PURE__ */ new Map();
|
|
486
|
+
this.hostnameCacheTtlMs = 3e4;
|
|
449
487
|
/**
|
|
450
488
|
* Lazily load the OpenAPI spec JSON shipped by @objectstack/spec.
|
|
451
489
|
* Cached after first read. Resilient to missing files / parse errors
|
|
@@ -466,6 +504,7 @@ var RestServer = class {
|
|
|
466
504
|
this.approvalsServiceProvider = approvalsServiceProvider;
|
|
467
505
|
this.sharingRulesServiceProvider = sharingRulesServiceProvider;
|
|
468
506
|
this.i18nServiceProvider = i18nServiceProvider;
|
|
507
|
+
this.analyticsServiceProvider = analyticsServiceProvider;
|
|
469
508
|
}
|
|
470
509
|
/**
|
|
471
510
|
* Resolve the protocol for a given request. When `environmentId` is present
|
|
@@ -489,13 +528,28 @@ var RestServer = class {
|
|
|
489
528
|
* (and any other client) speak a single, uniform URL family without
|
|
490
529
|
* duplicating route logic for the platform surface.
|
|
491
530
|
*/
|
|
531
|
+
/**
|
|
532
|
+
* Cached wrapper around `envRegistry.resolveByHostname` (P1-4). Returns the
|
|
533
|
+
* cached result while fresh; on a miss it queries the registry and caches the
|
|
534
|
+
* outcome (positive *and* negative) for {@link hostnameCacheTtlMs}. Registry
|
|
535
|
+
* errors are not cached so a transient control-plane blip self-heals on the
|
|
536
|
+
* next request.
|
|
537
|
+
*/
|
|
538
|
+
async resolveHostnameCached(host) {
|
|
539
|
+
const now = Date.now();
|
|
540
|
+
const hit = this.hostnameCache.get(host);
|
|
541
|
+
if (hit && hit.expiresAt > now) return hit.value;
|
|
542
|
+
const result = await this.envRegistry.resolveByHostname(host) ?? null;
|
|
543
|
+
this.hostnameCache.set(host, { value: result, expiresAt: now + this.hostnameCacheTtlMs });
|
|
544
|
+
return result;
|
|
545
|
+
}
|
|
492
546
|
async resolveProtocol(environmentId, req) {
|
|
493
547
|
if (environmentId === "platform") return this.protocol;
|
|
494
548
|
if (!environmentId && req && this.envRegistry && this.kernelManager) {
|
|
495
549
|
const host = this.extractHostname(req);
|
|
496
550
|
if (host) {
|
|
497
551
|
try {
|
|
498
|
-
const result = await this.
|
|
552
|
+
const result = await this.resolveHostnameCached(host);
|
|
499
553
|
if (result?.environmentId) environmentId = result.environmentId;
|
|
500
554
|
} catch {
|
|
501
555
|
}
|
|
@@ -539,7 +593,7 @@ var RestServer = class {
|
|
|
539
593
|
const host = this.extractHostname(req);
|
|
540
594
|
if (host) {
|
|
541
595
|
try {
|
|
542
|
-
const result = await this.
|
|
596
|
+
const result = await this.resolveHostnameCached(host);
|
|
543
597
|
if (result?.environmentId) environmentId = result.environmentId;
|
|
544
598
|
} catch {
|
|
545
599
|
}
|
|
@@ -610,7 +664,7 @@ var RestServer = class {
|
|
|
610
664
|
const host = this.extractHostname(req);
|
|
611
665
|
if (host) {
|
|
612
666
|
try {
|
|
613
|
-
const result = await this.
|
|
667
|
+
const result = await this.resolveHostnameCached(host);
|
|
614
668
|
if (result?.environmentId) environmentId = result.environmentId;
|
|
615
669
|
} catch {
|
|
616
670
|
}
|
|
@@ -664,13 +718,27 @@ var RestServer = class {
|
|
|
664
718
|
} else {
|
|
665
719
|
return void 0;
|
|
666
720
|
}
|
|
667
|
-
const session = await api.getSession({ headers });
|
|
668
|
-
if (!session?.user?.id) return void 0;
|
|
669
|
-
const userId = session.user.id;
|
|
670
|
-
const tenantId = session.session?.activeOrganizationId ?? void 0;
|
|
671
721
|
const permissions = [];
|
|
672
722
|
const systemPermissions = [];
|
|
673
723
|
const roles = [];
|
|
724
|
+
let identityQl;
|
|
725
|
+
if (kernel) identityQl = await kernel.getServiceAsync("objectql").catch(() => void 0);
|
|
726
|
+
if (!identityQl && this.objectQLProvider) {
|
|
727
|
+
identityQl = await this.objectQLProvider(environmentId).catch(() => void 0);
|
|
728
|
+
}
|
|
729
|
+
let userId;
|
|
730
|
+
let tenantId;
|
|
731
|
+
const keyPrincipal = await (0, import_core.resolveApiKeyPrincipal)(identityQl, headers).catch(() => void 0);
|
|
732
|
+
if (keyPrincipal) {
|
|
733
|
+
userId = keyPrincipal.userId;
|
|
734
|
+
tenantId = keyPrincipal.tenantId;
|
|
735
|
+
for (const s of keyPrincipal.scopes) if (!permissions.includes(s)) permissions.push(s);
|
|
736
|
+
} else {
|
|
737
|
+
const session = await api.getSession({ headers });
|
|
738
|
+
if (!session?.user?.id) return void 0;
|
|
739
|
+
userId = session.user.id;
|
|
740
|
+
tenantId = session.session?.activeOrganizationId ?? void 0;
|
|
741
|
+
}
|
|
674
742
|
try {
|
|
675
743
|
let ql;
|
|
676
744
|
if (kernel) {
|
|
@@ -1081,6 +1149,7 @@ var RestServer = class {
|
|
|
1081
1149
|
this.registerSharingRuleEndpoints(bp);
|
|
1082
1150
|
this.registerReportsEndpoints(bp);
|
|
1083
1151
|
this.registerApprovalsEndpoints(bp);
|
|
1152
|
+
this.registerAnalyticsEndpoints(bp);
|
|
1084
1153
|
if (this.config.api.enableCrud) {
|
|
1085
1154
|
this.registerCrudEndpoints(bp);
|
|
1086
1155
|
}
|
|
@@ -1121,6 +1190,13 @@ var RestServer = class {
|
|
|
1121
1190
|
if (this.config.api.enableUi) {
|
|
1122
1191
|
discovery.routes.ui = `${realBase}/ui`;
|
|
1123
1192
|
}
|
|
1193
|
+
const mcpEnabled = globalThis?.process?.env?.OS_MCP_SERVER_ENABLED === "true";
|
|
1194
|
+
if (mcpEnabled) {
|
|
1195
|
+
const unscopedBase = isScoped ? basePath.replace(/\/(environments|projects)\/:environmentId$/, "") : basePath;
|
|
1196
|
+
discovery.routes.mcp = `${unscopedBase}/mcp`;
|
|
1197
|
+
} else {
|
|
1198
|
+
delete discovery.routes.mcp;
|
|
1199
|
+
}
|
|
1124
1200
|
if (discovery.routes.auth) {
|
|
1125
1201
|
const unscopedBase = isScoped ? basePath.replace(/\/projects\/:environmentId$/, "") : basePath;
|
|
1126
1202
|
discovery.routes.auth = `${unscopedBase}/auth`;
|
|
@@ -1613,13 +1689,15 @@ var RestServer = class {
|
|
|
1613
1689
|
const actorHeader = req.headers?.["x-actor"] ?? req.headers?.["X-Actor"] ?? req.user?.id ?? req.userId;
|
|
1614
1690
|
const actor = typeof actorHeader === "string" ? actorHeader : void 0;
|
|
1615
1691
|
const stateParam = typeof req.query?.state === "string" && req.query.state.toLowerCase() === "draft" ? "draft" : void 0;
|
|
1692
|
+
const dropStorage = req.query?.dropStorage === "true" || req.query?.dropStorage === "1";
|
|
1616
1693
|
const result = await p.deleteMetaItem({
|
|
1617
1694
|
type: req.params.type,
|
|
1618
1695
|
name: req.params.name,
|
|
1619
1696
|
...environmentId ? { environmentId } : {},
|
|
1620
1697
|
...parentVersion !== void 0 ? { parentVersion } : {},
|
|
1621
1698
|
...actor ? { actor } : {},
|
|
1622
|
-
...stateParam ? { state: stateParam } : {}
|
|
1699
|
+
...stateParam ? { state: stateParam } : {},
|
|
1700
|
+
...dropStorage ? { dropStorage: true } : {}
|
|
1623
1701
|
});
|
|
1624
1702
|
res.json(result);
|
|
1625
1703
|
} catch (error) {
|
|
@@ -2868,6 +2946,89 @@ var RestServer = class {
|
|
|
2868
2946
|
* when no sharing service is configured so a deployment without the
|
|
2869
2947
|
* `@objectstack/plugin-sharing` plugin fails cleanly.
|
|
2870
2948
|
*/
|
|
2949
|
+
/**
|
|
2950
|
+
* ADR-0021 — analytics dataset preview/query endpoint.
|
|
2951
|
+
*
|
|
2952
|
+
* POST {basePath}/analytics/dataset/query
|
|
2953
|
+
* body: { dataset?: <inline Dataset>, datasetName?: string, selection: DatasetSelection }
|
|
2954
|
+
*
|
|
2955
|
+
* Compiles the dataset (an inline draft for Studio preview, or a saved one
|
|
2956
|
+
* by name) and runs the selection through the analytics service's
|
|
2957
|
+
* `queryDataset`, threading the request ExecutionContext so tenant/RLS
|
|
2958
|
+
* scoping (ADR-0021 D-C) applies. Returns 501 when no analytics service
|
|
2959
|
+
* (or one without `queryDataset`) is configured, so a deployment without
|
|
2960
|
+
* `@objectstack/service-analytics` fails cleanly.
|
|
2961
|
+
*/
|
|
2962
|
+
registerAnalyticsEndpoints(basePath) {
|
|
2963
|
+
const isScoped = basePath.includes("/environments/:environmentId");
|
|
2964
|
+
const resolveService = async (environmentId) => {
|
|
2965
|
+
if (!this.analyticsServiceProvider) return void 0;
|
|
2966
|
+
try {
|
|
2967
|
+
return await this.analyticsServiceProvider(environmentId);
|
|
2968
|
+
} catch {
|
|
2969
|
+
return void 0;
|
|
2970
|
+
}
|
|
2971
|
+
};
|
|
2972
|
+
this.routeManager.register({
|
|
2973
|
+
method: "POST",
|
|
2974
|
+
path: `${basePath}/analytics/dataset/query`,
|
|
2975
|
+
handler: async (req, res) => {
|
|
2976
|
+
try {
|
|
2977
|
+
const environmentId = isScoped ? req.params?.environmentId : void 0;
|
|
2978
|
+
const context = await this.resolveExecCtx(environmentId, req);
|
|
2979
|
+
if (this.enforceAuth(req, res, context)) return;
|
|
2980
|
+
const svc = await resolveService(environmentId);
|
|
2981
|
+
if (!svc || typeof svc.queryDataset !== "function") {
|
|
2982
|
+
return res.status(501).json({
|
|
2983
|
+
code: "NOT_IMPLEMENTED",
|
|
2984
|
+
message: "Analytics dataset query is not available on this deployment (no analytics service with queryDataset)."
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2987
|
+
const body = req.body ?? {};
|
|
2988
|
+
const selection = body.selection;
|
|
2989
|
+
if (!selection || !Array.isArray(selection.measures) || selection.measures.length === 0) {
|
|
2990
|
+
return res.status(400).json({
|
|
2991
|
+
code: "VALIDATION_FAILED",
|
|
2992
|
+
message: "body.selection.measures must be a non-empty array of measure names."
|
|
2993
|
+
});
|
|
2994
|
+
}
|
|
2995
|
+
let dataset = body.dataset;
|
|
2996
|
+
if (!dataset && body.datasetName) {
|
|
2997
|
+
const p = await this.resolveProtocol(environmentId, req);
|
|
2998
|
+
const items = await p.getMetaItems?.({ type: "dataset" }).catch(() => null);
|
|
2999
|
+
const list = Array.isArray(items?.items) ? items.items : Array.isArray(items) ? items : [];
|
|
3000
|
+
dataset = list.find((d) => d?.name === body.datasetName);
|
|
3001
|
+
if (!dataset) {
|
|
3002
|
+
return res.status(404).json({ code: "NOT_FOUND", message: `Dataset "${body.datasetName}" not found.` });
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
if (!dataset) {
|
|
3006
|
+
return res.status(400).json({ code: "VALIDATION_FAILED", message: "Provide body.dataset (inline) or body.datasetName." });
|
|
3007
|
+
}
|
|
3008
|
+
try {
|
|
3009
|
+
const { DatasetSchema } = await import("@objectstack/spec/ui");
|
|
3010
|
+
dataset = DatasetSchema.parse(dataset);
|
|
3011
|
+
} catch (verr) {
|
|
3012
|
+
return res.status(400).json({
|
|
3013
|
+
code: "VALIDATION_FAILED",
|
|
3014
|
+
message: "Invalid dataset definition.",
|
|
3015
|
+
detail: String(verr?.message ?? verr).slice(0, 1e3)
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
const result = await svc.queryDataset(dataset, selection, context ?? void 0);
|
|
3019
|
+
res.json(result);
|
|
3020
|
+
} catch (error) {
|
|
3021
|
+
const msg = String(error?.message ?? error ?? "");
|
|
3022
|
+
if (/not declared in the dataset|not backed by a declared relationship|not supported by the v1 dataset runtime|read-scope-sql/.test(msg)) {
|
|
3023
|
+
return res.status(400).json({ code: "DATASET_INVALID", message: msg.slice(0, 1e3) });
|
|
3024
|
+
}
|
|
3025
|
+
logError("[REST] Analytics dataset query error:", error);
|
|
3026
|
+
res.status(500).json({ code: "ANALYTICS_QUERY_FAILED", error: msg.slice(0, 500) });
|
|
3027
|
+
}
|
|
3028
|
+
},
|
|
3029
|
+
metadata: { summary: "Run a semantic-layer dataset (preview/query)", tags: ["analytics"] }
|
|
3030
|
+
});
|
|
3031
|
+
}
|
|
2871
3032
|
registerSharingEndpoints(basePath) {
|
|
2872
3033
|
const { crud } = this.config;
|
|
2873
3034
|
const dataPath = `${basePath}${crud.dataPrefix}`;
|
|
@@ -3409,11 +3570,13 @@ var RestServer = class {
|
|
|
3409
3570
|
return;
|
|
3410
3571
|
}
|
|
3411
3572
|
const q = req.query ?? {};
|
|
3573
|
+
const rawApprover = q.approverId ?? q.approver_id;
|
|
3574
|
+
const approverIds = (Array.isArray(rawApprover) ? rawApprover : rawApprover != null ? [rawApprover] : []).flatMap((s) => String(s).split(",")).map((s) => s.trim()).filter(Boolean);
|
|
3412
3575
|
const rows = await svc.listRequests({
|
|
3413
3576
|
object: q.object,
|
|
3414
3577
|
recordId: q.recordId ?? q.record_id,
|
|
3415
3578
|
status: q.status,
|
|
3416
|
-
approverId:
|
|
3579
|
+
approverId: approverIds.length ? approverIds : void 0,
|
|
3417
3580
|
submitterId: q.submitterId ?? q.submitter_id
|
|
3418
3581
|
}, context ?? {});
|
|
3419
3582
|
res.json({ data: rows });
|
|
@@ -3508,6 +3671,73 @@ var RestServer = class {
|
|
|
3508
3671
|
const dataPath = `${basePath}${crud.dataPrefix}`;
|
|
3509
3672
|
const isScoped = basePath.includes("/environments/:environmentId");
|
|
3510
3673
|
const operations = batch.operations;
|
|
3674
|
+
this.routeManager.register({
|
|
3675
|
+
method: "POST",
|
|
3676
|
+
path: `${basePath}/batch`,
|
|
3677
|
+
handler: async (req, res) => {
|
|
3678
|
+
try {
|
|
3679
|
+
const environmentId = isScoped ? req.params?.environmentId : void 0;
|
|
3680
|
+
const context = await this.resolveExecCtx(environmentId, req);
|
|
3681
|
+
if (this.enforceAuth(req, res, context)) return;
|
|
3682
|
+
const ql = this.objectQLProvider ? await this.objectQLProvider(environmentId) : void 0;
|
|
3683
|
+
if (!ql || typeof ql.transaction !== "function") {
|
|
3684
|
+
res.status(501).json({ error: "Transactional batch not supported by this runtime" });
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3687
|
+
const ops = Array.isArray(req.body?.operations) ? req.body.operations : [];
|
|
3688
|
+
const max = batch.maxBatchSize ?? 200;
|
|
3689
|
+
if (ops.length === 0) {
|
|
3690
|
+
res.json({ results: [] });
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
if (ops.length > max) {
|
|
3694
|
+
res.status(400).json({ error: `Batch too large (max ${max})` });
|
|
3695
|
+
return;
|
|
3696
|
+
}
|
|
3697
|
+
const resolveRefs = (data, out) => {
|
|
3698
|
+
if (!data || typeof data !== "object") return data;
|
|
3699
|
+
const result = Array.isArray(data) ? [] : {};
|
|
3700
|
+
for (const [k, v] of Object.entries(data)) {
|
|
3701
|
+
if (v && typeof v === "object" && "$ref" in v) {
|
|
3702
|
+
const ref = out[v.$ref];
|
|
3703
|
+
result[k] = (ref && (ref.id ?? ref._id)) ?? null;
|
|
3704
|
+
} else {
|
|
3705
|
+
result[k] = v;
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
return result;
|
|
3709
|
+
};
|
|
3710
|
+
const results = await ql.transaction(async (trxCtx) => {
|
|
3711
|
+
const out = [];
|
|
3712
|
+
for (const op of ops) {
|
|
3713
|
+
const action = String(op?.action || "create");
|
|
3714
|
+
const object = String(op?.object || "");
|
|
3715
|
+
if (!object) throw new Error("Each operation requires an `object`");
|
|
3716
|
+
const data = resolveRefs(op.data, out);
|
|
3717
|
+
if (action === "create") {
|
|
3718
|
+
out.push(await ql.insert(object, data, { context: trxCtx }));
|
|
3719
|
+
} else if (action === "update") {
|
|
3720
|
+
const id = op.id ?? data?.id;
|
|
3721
|
+
out.push(await ql.update(object, { ...data, id }, { context: trxCtx }));
|
|
3722
|
+
} else if (action === "delete") {
|
|
3723
|
+
out.push(await ql.delete(object, { where: { id: op.id }, context: trxCtx }));
|
|
3724
|
+
} else {
|
|
3725
|
+
throw new Error(`Unknown batch action: ${action}`);
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
return out;
|
|
3729
|
+
}, context);
|
|
3730
|
+
res.json({ results });
|
|
3731
|
+
} catch (error) {
|
|
3732
|
+
logError("[REST] Unhandled error:", error);
|
|
3733
|
+
sendError(res, error);
|
|
3734
|
+
}
|
|
3735
|
+
},
|
|
3736
|
+
metadata: {
|
|
3737
|
+
summary: "Cross-object transactional batch (atomic create/update/delete across objects)",
|
|
3738
|
+
tags: ["data", "batch"]
|
|
3739
|
+
}
|
|
3740
|
+
});
|
|
3511
3741
|
if (batch.enableBatchEndpoint && this.protocol.batchData) {
|
|
3512
3742
|
this.routeManager.register({
|
|
3513
3743
|
method: "POST",
|
|
@@ -3907,6 +4137,13 @@ function createRestApiPlugin(config = {}) {
|
|
|
3907
4137
|
return void 0;
|
|
3908
4138
|
}
|
|
3909
4139
|
};
|
|
4140
|
+
const analyticsServiceProvider = async (_environmentId) => {
|
|
4141
|
+
try {
|
|
4142
|
+
return ctx.getService("analytics");
|
|
4143
|
+
} catch {
|
|
4144
|
+
return void 0;
|
|
4145
|
+
}
|
|
4146
|
+
};
|
|
3910
4147
|
if (!server) {
|
|
3911
4148
|
ctx.logger.warn(`RestApiPlugin: HTTP Server service '${serverService}' not found. REST routes skipped.`);
|
|
3912
4149
|
return;
|
|
@@ -3917,7 +4154,7 @@ function createRestApiPlugin(config = {}) {
|
|
|
3917
4154
|
}
|
|
3918
4155
|
ctx.logger.info("Hydrating REST API from Protocol...");
|
|
3919
4156
|
try {
|
|
3920
|
-
const restServer = new RestServer(server, protocol, config.api, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider);
|
|
4157
|
+
const restServer = new RestServer(server, protocol, config.api, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider);
|
|
3921
4158
|
restServer.registerRoutes();
|
|
3922
4159
|
ctx.logger.info("REST API successfully registered");
|
|
3923
4160
|
} catch (err) {
|