@cadenza.io/service 2.11.0 → 2.15.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/README.md +19 -1
- package/dist/index.d.mts +54 -10
- package/dist/index.d.ts +54 -10
- package/dist/index.js +1761 -1353
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1762 -1354
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1649,7 +1649,7 @@ var ServiceRegistry = class _ServiceRegistry {
|
|
|
1649
1649
|
}
|
|
1650
1650
|
).emits("meta.service_registry.service_inserted").emitsOnFail("meta.service_registry.service_insertion_failed");
|
|
1651
1651
|
this.insertServiceInstanceTask = CadenzaService.createCadenzaDBInsertTask(
|
|
1652
|
-
"
|
|
1652
|
+
"service_instance",
|
|
1653
1653
|
{},
|
|
1654
1654
|
{
|
|
1655
1655
|
inputSchema: {
|
|
@@ -2593,20 +2593,44 @@ var RestController = class _RestController {
|
|
|
2593
2593
|
let ctx2;
|
|
2594
2594
|
ctx2 = req.body;
|
|
2595
2595
|
deputyExecId = ctx2.__metadata.__deputyExecId;
|
|
2596
|
+
const remoteRoutineName = ctx2.__remoteRoutineName;
|
|
2597
|
+
const targetNotFoundSignal = `meta.rest.delegation_target_not_found:${deputyExecId}`;
|
|
2598
|
+
let resolved = false;
|
|
2599
|
+
const resolveDelegation = (endCtx, status) => {
|
|
2600
|
+
if (resolved || res.headersSent) {
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
resolved = true;
|
|
2604
|
+
const metadata = endCtx?.__metadata && typeof endCtx.__metadata === "object" ? endCtx.__metadata : {};
|
|
2605
|
+
if (endCtx?.__metadata) {
|
|
2606
|
+
delete endCtx.__metadata;
|
|
2607
|
+
}
|
|
2608
|
+
res.json({
|
|
2609
|
+
...endCtx,
|
|
2610
|
+
...metadata,
|
|
2611
|
+
__status: status
|
|
2612
|
+
});
|
|
2613
|
+
};
|
|
2596
2614
|
CadenzaService.createEphemeralMetaTask(
|
|
2597
2615
|
"Resolve delegation",
|
|
2598
|
-
(endCtx) =>
|
|
2599
|
-
const metadata = endCtx.__metadata;
|
|
2600
|
-
delete endCtx.__metadata;
|
|
2601
|
-
res.json({
|
|
2602
|
-
...endCtx,
|
|
2603
|
-
...metadata,
|
|
2604
|
-
__status: "success"
|
|
2605
|
-
});
|
|
2606
|
-
},
|
|
2616
|
+
(endCtx) => resolveDelegation(endCtx, "success"),
|
|
2607
2617
|
"Resolves a delegation request",
|
|
2608
2618
|
{ register: false }
|
|
2609
2619
|
).doOn(`meta.node.graph_completed:${deputyExecId}`).emits(`meta.rest.delegation_resolved:${deputyExecId}`);
|
|
2620
|
+
CadenzaService.createEphemeralMetaTask(
|
|
2621
|
+
"Resolve delegation target lookup failure",
|
|
2622
|
+
(endCtx) => resolveDelegation(endCtx, "error"),
|
|
2623
|
+
"Resolves delegation requests that cannot find a local task or routine",
|
|
2624
|
+
{ register: false }
|
|
2625
|
+
).doOn(targetNotFoundSignal);
|
|
2626
|
+
if (!CadenzaService.get(remoteRoutineName) && !CadenzaService.registry.routines.get(remoteRoutineName)) {
|
|
2627
|
+
CadenzaService.emit(targetNotFoundSignal, {
|
|
2628
|
+
...ctx2,
|
|
2629
|
+
__error: `No task or routine registered for delegation target ${remoteRoutineName}.`,
|
|
2630
|
+
errored: true
|
|
2631
|
+
});
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2610
2634
|
CadenzaService.emit("meta.rest.delegation_requested", {
|
|
2611
2635
|
...ctx2,
|
|
2612
2636
|
__name: ctx2.__remoteRoutineName
|
|
@@ -2777,8 +2801,16 @@ var RestController = class _RestController {
|
|
|
2777
2801
|
CadenzaService.runner.run(routine, context);
|
|
2778
2802
|
return true;
|
|
2779
2803
|
} else {
|
|
2804
|
+
const deputyExecId = context.__metadata?.__deputyExecId ?? context.__deputyExecId;
|
|
2805
|
+
const remoteRoutineName = context.__remoteRoutineName ?? context.__name ?? "unknown";
|
|
2780
2806
|
context.errored = true;
|
|
2781
|
-
context.__error =
|
|
2807
|
+
context.__error = `No task or routine registered for delegation target ${remoteRoutineName}.`;
|
|
2808
|
+
if (deputyExecId) {
|
|
2809
|
+
emit(
|
|
2810
|
+
`meta.rest.delegation_target_not_found:${deputyExecId}`,
|
|
2811
|
+
context
|
|
2812
|
+
);
|
|
2813
|
+
}
|
|
2782
2814
|
emit("meta.runner.failed", context);
|
|
2783
2815
|
return false;
|
|
2784
2816
|
}
|
|
@@ -2801,25 +2833,25 @@ var RestController = class _RestController {
|
|
|
2801
2833
|
(ctx) => {
|
|
2802
2834
|
const { serviceName, serviceAddress, servicePort, protocol } = ctx;
|
|
2803
2835
|
const port = protocol === "https" ? 443 : servicePort;
|
|
2804
|
-
const
|
|
2836
|
+
const URL2 = `${protocol}://${serviceAddress}:${port}`;
|
|
2805
2837
|
const fetchId = `${serviceAddress}_${port}`;
|
|
2806
2838
|
const fetchDiagnostics = this.ensureFetchClientDiagnostics(
|
|
2807
2839
|
fetchId,
|
|
2808
2840
|
serviceName,
|
|
2809
|
-
|
|
2841
|
+
URL2
|
|
2810
2842
|
);
|
|
2811
2843
|
fetchDiagnostics.destroyed = false;
|
|
2812
2844
|
fetchDiagnostics.updatedAt = Date.now();
|
|
2813
|
-
if (CadenzaService.get(`Send Handshake to ${
|
|
2814
|
-
console.error("Fetch client already exists",
|
|
2845
|
+
if (CadenzaService.get(`Send Handshake to ${URL2}`)) {
|
|
2846
|
+
console.error("Fetch client already exists", URL2);
|
|
2815
2847
|
return;
|
|
2816
2848
|
}
|
|
2817
2849
|
const handshakeTask = CadenzaService.createMetaTask(
|
|
2818
|
-
`Send Handshake to ${
|
|
2850
|
+
`Send Handshake to ${URL2}`,
|
|
2819
2851
|
async (ctx2, emit) => {
|
|
2820
2852
|
try {
|
|
2821
2853
|
const response = await this.fetchDataWithTimeout(
|
|
2822
|
-
`${
|
|
2854
|
+
`${URL2}/handshake`,
|
|
2823
2855
|
{
|
|
2824
2856
|
headers: {
|
|
2825
2857
|
"Content-Type": "application/json"
|
|
@@ -2834,10 +2866,10 @@ var RestController = class _RestController {
|
|
|
2834
2866
|
fetchDiagnostics.connected = false;
|
|
2835
2867
|
fetchDiagnostics.lastHandshakeError = error;
|
|
2836
2868
|
fetchDiagnostics.updatedAt = Date.now();
|
|
2837
|
-
this.recordFetchClientError(fetchId, serviceName,
|
|
2869
|
+
this.recordFetchClientError(fetchId, serviceName, URL2, error);
|
|
2838
2870
|
CadenzaService.log(
|
|
2839
2871
|
"Fetch handshake failed.",
|
|
2840
|
-
{ error, serviceName, URL },
|
|
2872
|
+
{ error, serviceName, URL: URL2 },
|
|
2841
2873
|
"warning"
|
|
2842
2874
|
);
|
|
2843
2875
|
emit(`meta.fetch.handshake_failed:${fetchId}`, response);
|
|
@@ -2852,7 +2884,7 @@ var RestController = class _RestController {
|
|
|
2852
2884
|
CadenzaService.log("Fetch client connected.", {
|
|
2853
2885
|
response,
|
|
2854
2886
|
serviceName,
|
|
2855
|
-
URL
|
|
2887
|
+
URL: URL2
|
|
2856
2888
|
});
|
|
2857
2889
|
for (const communicationType of ctx2.communicationTypes) {
|
|
2858
2890
|
emit("global.meta.fetch.service_communication_established", {
|
|
@@ -2867,10 +2899,10 @@ var RestController = class _RestController {
|
|
|
2867
2899
|
fetchDiagnostics.connected = false;
|
|
2868
2900
|
fetchDiagnostics.lastHandshakeError = this.getErrorMessage(e);
|
|
2869
2901
|
fetchDiagnostics.updatedAt = Date.now();
|
|
2870
|
-
this.recordFetchClientError(fetchId, serviceName,
|
|
2902
|
+
this.recordFetchClientError(fetchId, serviceName, URL2, e);
|
|
2871
2903
|
CadenzaService.log(
|
|
2872
2904
|
"Error in fetch handshake",
|
|
2873
|
-
{ error: e, serviceName, URL, ctx: ctx2 },
|
|
2905
|
+
{ error: e, serviceName, URL: URL2, ctx: ctx2 },
|
|
2874
2906
|
"error"
|
|
2875
2907
|
);
|
|
2876
2908
|
return { ...ctx2, __error: e, errored: true };
|
|
@@ -2884,7 +2916,7 @@ var RestController = class _RestController {
|
|
|
2884
2916
|
"global.meta.fetch.service_communication_established"
|
|
2885
2917
|
);
|
|
2886
2918
|
const delegateTask = CadenzaService.createMetaTask(
|
|
2887
|
-
`Delegate flow to REST server ${
|
|
2919
|
+
`Delegate flow to REST server ${URL2}`,
|
|
2888
2920
|
async (ctx2, emit) => {
|
|
2889
2921
|
if (ctx2.__remoteRoutineName === void 0) {
|
|
2890
2922
|
return;
|
|
@@ -2894,7 +2926,7 @@ var RestController = class _RestController {
|
|
|
2894
2926
|
let resultContext;
|
|
2895
2927
|
try {
|
|
2896
2928
|
resultContext = await this.fetchDataWithTimeout(
|
|
2897
|
-
`${
|
|
2929
|
+
`${URL2}/delegation`,
|
|
2898
2930
|
{
|
|
2899
2931
|
headers: {
|
|
2900
2932
|
"Content-Type": "application/json"
|
|
@@ -2910,7 +2942,7 @@ var RestController = class _RestController {
|
|
|
2910
2942
|
this.recordFetchClientError(
|
|
2911
2943
|
fetchId,
|
|
2912
2944
|
serviceName,
|
|
2913
|
-
|
|
2945
|
+
URL2,
|
|
2914
2946
|
resultContext?.__error ?? resultContext?.error ?? "Delegation failed"
|
|
2915
2947
|
);
|
|
2916
2948
|
}
|
|
@@ -2918,7 +2950,7 @@ var RestController = class _RestController {
|
|
|
2918
2950
|
console.error("Error in delegation", e);
|
|
2919
2951
|
fetchDiagnostics.delegationFailures++;
|
|
2920
2952
|
fetchDiagnostics.updatedAt = Date.now();
|
|
2921
|
-
this.recordFetchClientError(fetchId, serviceName,
|
|
2953
|
+
this.recordFetchClientError(fetchId, serviceName, URL2, e);
|
|
2922
2954
|
resultContext = {
|
|
2923
2955
|
__error: `Error: ${e}`,
|
|
2924
2956
|
errored: true,
|
|
@@ -2939,7 +2971,7 @@ var RestController = class _RestController {
|
|
|
2939
2971
|
`meta.service_registry.socket_failed:${fetchId}`
|
|
2940
2972
|
).emitsOnFail("meta.fetch.delegate_failed").attachSignal("meta.fetch.delegated");
|
|
2941
2973
|
const transmitTask = CadenzaService.createMetaTask(
|
|
2942
|
-
`Transmit signal to server ${
|
|
2974
|
+
`Transmit signal to server ${URL2}`,
|
|
2943
2975
|
async (ctx2, emit) => {
|
|
2944
2976
|
if (ctx2.__signalName === void 0) {
|
|
2945
2977
|
return;
|
|
@@ -2949,7 +2981,7 @@ var RestController = class _RestController {
|
|
|
2949
2981
|
let response;
|
|
2950
2982
|
try {
|
|
2951
2983
|
response = await this.fetchDataWithTimeout(
|
|
2952
|
-
`${
|
|
2984
|
+
`${URL2}/signal`,
|
|
2953
2985
|
{
|
|
2954
2986
|
headers: {
|
|
2955
2987
|
"Content-Type": "application/json"
|
|
@@ -2968,7 +3000,7 @@ var RestController = class _RestController {
|
|
|
2968
3000
|
this.recordFetchClientError(
|
|
2969
3001
|
fetchId,
|
|
2970
3002
|
serviceName,
|
|
2971
|
-
|
|
3003
|
+
URL2,
|
|
2972
3004
|
response?.__error ?? response?.error ?? "Signal transmission failed"
|
|
2973
3005
|
);
|
|
2974
3006
|
}
|
|
@@ -2976,7 +3008,7 @@ var RestController = class _RestController {
|
|
|
2976
3008
|
console.error("Error in transmission", e);
|
|
2977
3009
|
fetchDiagnostics.signalFailures++;
|
|
2978
3010
|
fetchDiagnostics.updatedAt = Date.now();
|
|
2979
|
-
this.recordFetchClientError(fetchId, serviceName,
|
|
3011
|
+
this.recordFetchClientError(fetchId, serviceName, URL2, e);
|
|
2980
3012
|
response = {
|
|
2981
3013
|
__error: `Error: ${e}`,
|
|
2982
3014
|
errored: true,
|
|
@@ -2992,14 +3024,14 @@ var RestController = class _RestController {
|
|
|
2992
3024
|
"meta.signal_controller.wildcard_signal_registered"
|
|
2993
3025
|
).emitsOnFail("meta.fetch.signal_transmission_failed").attachSignal("meta.fetch.transmitted");
|
|
2994
3026
|
const statusTask = CadenzaService.createMetaTask(
|
|
2995
|
-
`Request status from ${
|
|
3027
|
+
`Request status from ${URL2}`,
|
|
2996
3028
|
async (ctx2) => {
|
|
2997
3029
|
fetchDiagnostics.statusChecks++;
|
|
2998
3030
|
fetchDiagnostics.updatedAt = Date.now();
|
|
2999
3031
|
let status;
|
|
3000
3032
|
try {
|
|
3001
3033
|
status = await this.fetchDataWithTimeout(
|
|
3002
|
-
`${
|
|
3034
|
+
`${URL2}/status`,
|
|
3003
3035
|
{
|
|
3004
3036
|
method: "GET"
|
|
3005
3037
|
},
|
|
@@ -3011,14 +3043,14 @@ var RestController = class _RestController {
|
|
|
3011
3043
|
this.recordFetchClientError(
|
|
3012
3044
|
fetchId,
|
|
3013
3045
|
serviceName,
|
|
3014
|
-
|
|
3046
|
+
URL2,
|
|
3015
3047
|
status?.__error ?? status?.error ?? "Status check failed"
|
|
3016
3048
|
);
|
|
3017
3049
|
}
|
|
3018
3050
|
} catch (e) {
|
|
3019
3051
|
fetchDiagnostics.statusFailures++;
|
|
3020
3052
|
fetchDiagnostics.updatedAt = Date.now();
|
|
3021
|
-
this.recordFetchClientError(fetchId, serviceName,
|
|
3053
|
+
this.recordFetchClientError(fetchId, serviceName, URL2, e);
|
|
3022
3054
|
status = {
|
|
3023
3055
|
__error: `Error: ${e}`,
|
|
3024
3056
|
errored: true,
|
|
@@ -3033,7 +3065,7 @@ var RestController = class _RestController {
|
|
|
3033
3065
|
fetchDiagnostics.connected = false;
|
|
3034
3066
|
fetchDiagnostics.destroyed = true;
|
|
3035
3067
|
fetchDiagnostics.updatedAt = Date.now();
|
|
3036
|
-
CadenzaService.log("Destroying fetch client", { URL, serviceName });
|
|
3068
|
+
CadenzaService.log("Destroying fetch client", { URL: URL2, serviceName });
|
|
3037
3069
|
handshakeTask.destroy();
|
|
3038
3070
|
delegateTask.destroy();
|
|
3039
3071
|
transmitTask.destroy();
|
|
@@ -3435,13 +3467,13 @@ var SocketController = class _SocketController {
|
|
|
3435
3467
|
Object.assign(base, patch);
|
|
3436
3468
|
base.fetchId = fetchId;
|
|
3437
3469
|
base.updatedAt = now;
|
|
3438
|
-
const
|
|
3439
|
-
if (
|
|
3440
|
-
base.lastError =
|
|
3470
|
+
const errorMessage2 = input.error !== void 0 ? this.getErrorMessage(input.error) : void 0;
|
|
3471
|
+
if (errorMessage2) {
|
|
3472
|
+
base.lastError = errorMessage2;
|
|
3441
3473
|
base.lastErrorAt = now;
|
|
3442
3474
|
base.errorHistory.push({
|
|
3443
3475
|
at: new Date(now).toISOString(),
|
|
3444
|
-
message:
|
|
3476
|
+
message: errorMessage2
|
|
3445
3477
|
});
|
|
3446
3478
|
if (base.errorHistory.length > this.diagnosticsErrorHistoryLimit) {
|
|
3447
3479
|
base.errorHistory.splice(
|
|
@@ -3943,12 +3975,12 @@ var SocketController = class _SocketController {
|
|
|
3943
3975
|
if (ack) ack(response);
|
|
3944
3976
|
resolve(response);
|
|
3945
3977
|
};
|
|
3946
|
-
const resolveWithError = (
|
|
3978
|
+
const resolveWithError = (errorMessage2, fallbackError) => {
|
|
3947
3979
|
settle({
|
|
3948
3980
|
...data,
|
|
3949
3981
|
errored: true,
|
|
3950
|
-
__error:
|
|
3951
|
-
error: fallbackError instanceof Error ? fallbackError.message :
|
|
3982
|
+
__error: errorMessage2,
|
|
3983
|
+
error: fallbackError instanceof Error ? fallbackError.message : errorMessage2,
|
|
3952
3984
|
socketId: runtimeHandle.socket.id,
|
|
3953
3985
|
serviceName,
|
|
3954
3986
|
url
|
|
@@ -4253,19 +4285,19 @@ var SocketController = class _SocketController {
|
|
|
4253
4285
|
url
|
|
4254
4286
|
});
|
|
4255
4287
|
} else {
|
|
4256
|
-
const
|
|
4288
|
+
const errorMessage2 = result?.__error ?? result?.error ?? "Socket handshake failed";
|
|
4257
4289
|
upsertDiagnostics(
|
|
4258
4290
|
{
|
|
4259
4291
|
connected: false,
|
|
4260
4292
|
handshake: false,
|
|
4261
|
-
lastHandshakeError:
|
|
4293
|
+
lastHandshakeError: errorMessage2
|
|
4262
4294
|
},
|
|
4263
|
-
|
|
4295
|
+
errorMessage2
|
|
4264
4296
|
);
|
|
4265
4297
|
applySessionOperation("handshake", {
|
|
4266
4298
|
connected: false,
|
|
4267
4299
|
handshake: false,
|
|
4268
|
-
lastHandshakeError:
|
|
4300
|
+
lastHandshakeError: errorMessage2
|
|
4269
4301
|
});
|
|
4270
4302
|
CadenzaService.log(
|
|
4271
4303
|
"Socket handshake failed",
|
|
@@ -4313,15 +4345,15 @@ var SocketController = class _SocketController {
|
|
|
4313
4345
|
});
|
|
4314
4346
|
}
|
|
4315
4347
|
if (resultContext?.errored || resultContext?.failed) {
|
|
4316
|
-
const
|
|
4348
|
+
const errorMessage2 = resultContext?.__error ?? resultContext?.error ?? "Socket delegation failed";
|
|
4317
4349
|
upsertDiagnostics(
|
|
4318
4350
|
{
|
|
4319
|
-
lastHandshakeError: String(
|
|
4351
|
+
lastHandshakeError: String(errorMessage2)
|
|
4320
4352
|
},
|
|
4321
|
-
|
|
4353
|
+
errorMessage2
|
|
4322
4354
|
);
|
|
4323
4355
|
applySessionOperation("delegate", {
|
|
4324
|
-
lastHandshakeError: String(
|
|
4356
|
+
lastHandshakeError: String(errorMessage2)
|
|
4325
4357
|
});
|
|
4326
4358
|
}
|
|
4327
4359
|
return resultContext;
|
|
@@ -5155,566 +5187,175 @@ var SCHEMA_TYPES = [
|
|
|
5155
5187
|
|
|
5156
5188
|
// src/database/DatabaseController.ts
|
|
5157
5189
|
import { Pool } from "pg";
|
|
5158
|
-
import { camelCase, snakeCase } from "lodash-es";
|
|
5159
|
-
function
|
|
5160
|
-
const
|
|
5161
|
-
|
|
5162
|
-
|
|
5190
|
+
import { camelCase, kebabCase, snakeCase } from "lodash-es";
|
|
5191
|
+
function normalizeIntentToken(value) {
|
|
5192
|
+
const normalized = kebabCase(String(value ?? "").trim());
|
|
5193
|
+
if (!normalized) {
|
|
5194
|
+
throw new Error("Actor token cannot be empty");
|
|
5195
|
+
}
|
|
5196
|
+
return normalized;
|
|
5197
|
+
}
|
|
5198
|
+
function buildPostgresActorName(name) {
|
|
5199
|
+
return `${String(name ?? "").trim()}PostgresActor`;
|
|
5200
|
+
}
|
|
5201
|
+
function validateIntentName(intentName) {
|
|
5202
|
+
if (!intentName || typeof intentName !== "string") {
|
|
5203
|
+
throw new Error("Intent name must be a non-empty string");
|
|
5204
|
+
}
|
|
5205
|
+
if (intentName.length > 100) {
|
|
5206
|
+
throw new Error(`Intent name must be <= 100 characters: ${intentName}`);
|
|
5207
|
+
}
|
|
5208
|
+
if (intentName.includes(" ") || intentName.includes(".") || intentName.includes("\\")) {
|
|
5209
|
+
throw new Error(
|
|
5210
|
+
`Intent name cannot contain spaces, dots or backslashes: ${intentName}`
|
|
5211
|
+
);
|
|
5212
|
+
}
|
|
5213
|
+
}
|
|
5214
|
+
function defaultOperationIntentDescription(operation, tableName) {
|
|
5215
|
+
return `Perform a ${operation} operation on the ${tableName} table`;
|
|
5216
|
+
}
|
|
5217
|
+
function readCustomIntentConfig(customIntent) {
|
|
5218
|
+
if (typeof customIntent === "string") {
|
|
5219
|
+
return {
|
|
5220
|
+
intent: customIntent
|
|
5221
|
+
};
|
|
5222
|
+
}
|
|
5223
|
+
return {
|
|
5224
|
+
intent: customIntent.intent,
|
|
5225
|
+
description: customIntent.description,
|
|
5226
|
+
input: customIntent.input
|
|
5227
|
+
};
|
|
5228
|
+
}
|
|
5229
|
+
function resolveTableOperationIntents(actorName, tableName, table, operation, defaultInputSchema) {
|
|
5230
|
+
const actorToken = normalizeIntentToken(actorName);
|
|
5231
|
+
const defaultIntentName = `${operation}-pg-${actorToken}-${tableName}`;
|
|
5232
|
+
validateIntentName(defaultIntentName);
|
|
5163
5233
|
const intents = [
|
|
5164
5234
|
{
|
|
5165
5235
|
name: defaultIntentName,
|
|
5166
|
-
description:
|
|
5236
|
+
description: defaultOperationIntentDescription(operation, tableName),
|
|
5167
5237
|
input: defaultInputSchema
|
|
5168
5238
|
}
|
|
5169
5239
|
];
|
|
5170
|
-
const
|
|
5171
|
-
const
|
|
5172
|
-
for (const
|
|
5173
|
-
const
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
continue;
|
|
5179
|
-
}
|
|
5180
|
-
if (name.length > 100) {
|
|
5181
|
-
warnings.push(
|
|
5182
|
-
`Skipped custom query intent '${name}' for table '${tableName}': name must be <= 100 characters.`
|
|
5183
|
-
);
|
|
5184
|
-
continue;
|
|
5185
|
-
}
|
|
5186
|
-
if (name.includes(" ") || name.includes(".") || name.includes("\\")) {
|
|
5187
|
-
warnings.push(
|
|
5188
|
-
`Skipped custom query intent '${name}' for table '${tableName}': name cannot contain spaces, dots or backslashes.`
|
|
5240
|
+
const seenNames = /* @__PURE__ */ new Set([defaultIntentName]);
|
|
5241
|
+
const customIntentList = table.customIntents?.[operation] ?? [];
|
|
5242
|
+
for (const rawCustomIntent of customIntentList) {
|
|
5243
|
+
const customIntent = readCustomIntentConfig(rawCustomIntent);
|
|
5244
|
+
const intentName = String(customIntent.intent ?? "").trim();
|
|
5245
|
+
if (!intentName) {
|
|
5246
|
+
throw new Error(
|
|
5247
|
+
`Invalid custom ${operation} intent on table '${tableName}': intent must be a non-empty string`
|
|
5189
5248
|
);
|
|
5190
|
-
continue;
|
|
5191
5249
|
}
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5250
|
+
validateIntentName(intentName);
|
|
5251
|
+
if (seenNames.has(intentName)) {
|
|
5252
|
+
throw new Error(
|
|
5253
|
+
`Duplicate ${operation} intent '${intentName}' on table '${tableName}'`
|
|
5195
5254
|
);
|
|
5196
|
-
continue;
|
|
5197
5255
|
}
|
|
5198
|
-
|
|
5256
|
+
seenNames.add(intentName);
|
|
5199
5257
|
intents.push({
|
|
5200
|
-
name,
|
|
5201
|
-
description:
|
|
5202
|
-
input:
|
|
5258
|
+
name: intentName,
|
|
5259
|
+
description: customIntent.description ?? defaultOperationIntentDescription(operation, tableName),
|
|
5260
|
+
input: customIntent.input ?? defaultInputSchema
|
|
5203
5261
|
});
|
|
5204
5262
|
}
|
|
5205
|
-
return { intents
|
|
5263
|
+
return { intents };
|
|
5264
|
+
}
|
|
5265
|
+
function ensurePlainObject(value, label) {
|
|
5266
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
5267
|
+
throw new Error(`${label} must be an object`);
|
|
5268
|
+
}
|
|
5269
|
+
return value;
|
|
5270
|
+
}
|
|
5271
|
+
function sleep(timeoutMs) {
|
|
5272
|
+
return new Promise((resolve) => {
|
|
5273
|
+
setTimeout(resolve, timeoutMs);
|
|
5274
|
+
});
|
|
5275
|
+
}
|
|
5276
|
+
function normalizePositiveInteger(value, fallback, min = 1) {
|
|
5277
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
5278
|
+
return fallback;
|
|
5279
|
+
}
|
|
5280
|
+
const normalized = Math.trunc(value);
|
|
5281
|
+
if (normalized < min) {
|
|
5282
|
+
return fallback;
|
|
5283
|
+
}
|
|
5284
|
+
return normalized;
|
|
5285
|
+
}
|
|
5286
|
+
function errorMessage(error) {
|
|
5287
|
+
if (error instanceof Error) {
|
|
5288
|
+
return error.message;
|
|
5289
|
+
}
|
|
5290
|
+
return String(error);
|
|
5291
|
+
}
|
|
5292
|
+
function isTransientDatabaseError(error) {
|
|
5293
|
+
if (!error || typeof error !== "object") {
|
|
5294
|
+
return false;
|
|
5295
|
+
}
|
|
5296
|
+
const dbError = error;
|
|
5297
|
+
const code = String(dbError.code ?? "");
|
|
5298
|
+
if (["40001", "40P01", "57P03", "53300", "08006", "08001"].includes(code)) {
|
|
5299
|
+
return true;
|
|
5300
|
+
}
|
|
5301
|
+
const message = String(dbError.message ?? "").toLowerCase();
|
|
5302
|
+
return message.includes("timeout") || message.includes("terminating connection") || message.includes("connection reset");
|
|
5303
|
+
}
|
|
5304
|
+
function isSqlIdentifier(value) {
|
|
5305
|
+
return /^[a-z_][a-z0-9_]*$/.test(value);
|
|
5306
|
+
}
|
|
5307
|
+
function toSafeSqlIdentifier(value, fallback) {
|
|
5308
|
+
const normalized = snakeCase(value || "").replace(/[^a-z0-9_]/g, "_");
|
|
5309
|
+
const candidate = normalized || fallback;
|
|
5310
|
+
if (!isSqlIdentifier(candidate)) {
|
|
5311
|
+
throw new Error(`Invalid SQL identifier: ${value}`);
|
|
5312
|
+
}
|
|
5313
|
+
return candidate;
|
|
5314
|
+
}
|
|
5315
|
+
function isSupportedAggregateFunction(value) {
|
|
5316
|
+
const fn = String(value ?? "").toLowerCase();
|
|
5317
|
+
return ["count", "sum", "avg", "min", "max"].includes(fn);
|
|
5318
|
+
}
|
|
5319
|
+
function buildAggregateAlias(aggregate, index) {
|
|
5320
|
+
const fn = String(aggregate.fn ?? "").toLowerCase();
|
|
5321
|
+
const hasField = typeof aggregate.field === "string" && aggregate.field.trim().length > 0;
|
|
5322
|
+
return toSafeSqlIdentifier(
|
|
5323
|
+
String(aggregate.as ?? `${fn}_${hasField ? aggregate.field : "all"}_${index}`),
|
|
5324
|
+
`${fn || "aggregate"}_${index}`
|
|
5325
|
+
);
|
|
5326
|
+
}
|
|
5327
|
+
function resolveDataRows(data) {
|
|
5328
|
+
if (!data) {
|
|
5329
|
+
return [];
|
|
5330
|
+
}
|
|
5331
|
+
if (Array.isArray(data)) {
|
|
5332
|
+
return data.map((entry) => ensurePlainObject(entry, "data item"));
|
|
5333
|
+
}
|
|
5334
|
+
return [ensurePlainObject(data, "data")];
|
|
5206
5335
|
}
|
|
5207
5336
|
var DatabaseController = class _DatabaseController {
|
|
5208
|
-
/**
|
|
5209
|
-
* Constructor for initializing the `DatabaseService` class.
|
|
5210
|
-
*
|
|
5211
|
-
* This constructor method initializes a sequence of meta tasks to perform the following database-related operations:
|
|
5212
|
-
*
|
|
5213
|
-
* 1. **Database Creation**: Creates a new database with the specified name if it doesn't already exist.
|
|
5214
|
-
* Validates the database name to ensure it conforms to the required format.
|
|
5215
|
-
* 2. **Database Schema Validation**: Validates the structure and constraints of the schema definition provided.
|
|
5216
|
-
* 3. **Table Dependency Management**: Sorts tables within the schema by their dependencies to ensure proper creation order.
|
|
5217
|
-
* 4. **Schema Definition Processing**:
|
|
5218
|
-
* - Converts schema definitions into Data Definition Language (DDL) based on table and field specifications.
|
|
5219
|
-
* - Handles constraints, relationships, and field attributes such as uniqueness, primary keys, nullable fields, etc.
|
|
5220
|
-
* 5. **Index and Primary Key Definition**: Generates SQL for indices and primary keys based on the schema configuration.
|
|
5221
|
-
*
|
|
5222
|
-
* These tasks are encapsulated within a meta routine to provide a structured and procedural approach to database initialization and schema management.
|
|
5223
|
-
*/
|
|
5224
5337
|
constructor() {
|
|
5225
|
-
this.
|
|
5226
|
-
this.
|
|
5338
|
+
this.registrationsByActorName = /* @__PURE__ */ new Map();
|
|
5339
|
+
this.registrationsByActorToken = /* @__PURE__ */ new Map();
|
|
5340
|
+
this.adminDbClient = new Pool({
|
|
5227
5341
|
connectionString: process.env.DATABASE_ADDRESS ?? "",
|
|
5228
5342
|
database: "postgres",
|
|
5229
5343
|
ssl: {
|
|
5230
5344
|
rejectUnauthorized: false
|
|
5231
|
-
// ← This bypasses the chain validation error
|
|
5232
5345
|
}
|
|
5233
5346
|
});
|
|
5234
|
-
CadenzaService.
|
|
5235
|
-
"
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
throw new Error(
|
|
5247
|
-
`Invalid database name ${databaseName}. Names must only contain lowercase alphanumeric characters and underscores`
|
|
5248
|
-
);
|
|
5249
|
-
}
|
|
5250
|
-
console.log(`Creating database ${databaseName}`, {
|
|
5251
|
-
connectionString: process.env.DATABASE_ADDRESS ?? "",
|
|
5252
|
-
database: "postgres"
|
|
5253
|
-
});
|
|
5254
|
-
await this.dbClient.query(`CREATE DATABASE ${databaseName}`);
|
|
5255
|
-
console.log(`Database ${databaseName} created`);
|
|
5256
|
-
this.dbClient = new Pool({
|
|
5257
|
-
connectionString: process.env.DATABASE_ADDRESS ? process.env.DATABASE_ADDRESS.slice(
|
|
5258
|
-
0,
|
|
5259
|
-
process.env.DATABASE_ADDRESS.lastIndexOf("/")
|
|
5260
|
-
) + "/" + databaseName + "?sslmode=disable" : "",
|
|
5261
|
-
ssl: {
|
|
5262
|
-
rejectUnauthorized: false
|
|
5263
|
-
// ← This bypasses the chain validation error
|
|
5264
|
-
}
|
|
5265
|
-
});
|
|
5266
|
-
this.databaseName = databaseName;
|
|
5267
|
-
return true;
|
|
5268
|
-
} catch (error) {
|
|
5269
|
-
if (error.code === "42P04") {
|
|
5270
|
-
console.log("Database already exists");
|
|
5271
|
-
return true;
|
|
5272
|
-
}
|
|
5273
|
-
console.error("Failed to create database", error);
|
|
5274
|
-
throw new Error(`Failed to create database: ${error.message}`);
|
|
5275
|
-
}
|
|
5276
|
-
},
|
|
5277
|
-
"Creates the target database if it doesn't exist"
|
|
5278
|
-
).then(
|
|
5279
|
-
CadenzaService.createMetaTask(
|
|
5280
|
-
"Validate schema",
|
|
5281
|
-
(ctx) => {
|
|
5282
|
-
const { schema } = ctx;
|
|
5283
|
-
if (!schema?.tables || typeof schema.tables !== "object") {
|
|
5284
|
-
throw new Error("Invalid schema: missing or invalid tables");
|
|
5285
|
-
}
|
|
5286
|
-
for (const [tableName, table] of Object.entries(schema.tables)) {
|
|
5287
|
-
if (!table.fields || typeof table.fields !== "object") {
|
|
5288
|
-
console.log(tableName, "missing fields");
|
|
5289
|
-
throw new Error(`Invalid table ${tableName}: missing fields`);
|
|
5290
|
-
}
|
|
5291
|
-
for (const [fieldName, field] of Object.entries(table.fields)) {
|
|
5292
|
-
if (!fieldName.split("").every((c) => /[a-z_]/.test(c))) {
|
|
5293
|
-
console.log(tableName, "field not lowercase", fieldName);
|
|
5294
|
-
throw new Error(
|
|
5295
|
-
`Invalid field name ${fieldName} for ${tableName}. Field names must only contain lowercase alphanumeric characters and underscores`
|
|
5296
|
-
);
|
|
5297
|
-
}
|
|
5298
|
-
if (!Object.values(SCHEMA_TYPES).includes(field.type)) {
|
|
5299
|
-
console.log(
|
|
5300
|
-
tableName,
|
|
5301
|
-
"field invalid type",
|
|
5302
|
-
fieldName,
|
|
5303
|
-
field.type
|
|
5304
|
-
);
|
|
5305
|
-
throw new Error(
|
|
5306
|
-
`Invalid type ${field.type} for ${tableName}.${fieldName}`
|
|
5307
|
-
);
|
|
5308
|
-
}
|
|
5309
|
-
if (field.references && !field.references.match(/^[\w]+[(\w)]+$/)) {
|
|
5310
|
-
console.log(
|
|
5311
|
-
tableName,
|
|
5312
|
-
"invalid reference",
|
|
5313
|
-
fieldName,
|
|
5314
|
-
field.references
|
|
5315
|
-
);
|
|
5316
|
-
throw new Error(
|
|
5317
|
-
`Invalid reference ${field.references} for ${tableName}.${fieldName}`
|
|
5318
|
-
);
|
|
5319
|
-
}
|
|
5320
|
-
if (table.customSignals) {
|
|
5321
|
-
for (const op of ["query", "insert", "update", "delete"]) {
|
|
5322
|
-
const triggers = table.customSignals.triggers?.[op];
|
|
5323
|
-
const emissions = table.customSignals.emissions?.[op];
|
|
5324
|
-
if (triggers && !Array.isArray(triggers) && typeof triggers !== "object") {
|
|
5325
|
-
console.log(
|
|
5326
|
-
tableName,
|
|
5327
|
-
"invalid triggers",
|
|
5328
|
-
op,
|
|
5329
|
-
triggers
|
|
5330
|
-
);
|
|
5331
|
-
throw new Error(
|
|
5332
|
-
`Invalid triggers for ${tableName}.${op}`
|
|
5333
|
-
);
|
|
5334
|
-
}
|
|
5335
|
-
if (emissions && !Array.isArray(emissions) && typeof emissions !== "object") {
|
|
5336
|
-
console.log(
|
|
5337
|
-
tableName,
|
|
5338
|
-
"invalid emissions",
|
|
5339
|
-
op,
|
|
5340
|
-
emissions
|
|
5341
|
-
);
|
|
5342
|
-
throw new Error(
|
|
5343
|
-
`Invalid emissions for ${tableName}.${op}`
|
|
5344
|
-
);
|
|
5345
|
-
}
|
|
5346
|
-
}
|
|
5347
|
-
}
|
|
5348
|
-
if (table.customIntents?.query) {
|
|
5349
|
-
if (!Array.isArray(table.customIntents.query)) {
|
|
5350
|
-
throw new Error(
|
|
5351
|
-
`Invalid customIntents.query for ${tableName}: expected array`
|
|
5352
|
-
);
|
|
5353
|
-
}
|
|
5354
|
-
for (const customIntent of table.customIntents.query) {
|
|
5355
|
-
if (typeof customIntent !== "string" && (typeof customIntent !== "object" || !customIntent || typeof customIntent.intent !== "string")) {
|
|
5356
|
-
throw new Error(
|
|
5357
|
-
`Invalid custom query intent on ${tableName}: expected string or object with intent`
|
|
5358
|
-
);
|
|
5359
|
-
}
|
|
5360
|
-
}
|
|
5361
|
-
}
|
|
5362
|
-
}
|
|
5363
|
-
}
|
|
5364
|
-
console.log("SCHEMA VALIDATED");
|
|
5365
|
-
return true;
|
|
5366
|
-
},
|
|
5367
|
-
"Validates database schema structure and content"
|
|
5368
|
-
).then(
|
|
5369
|
-
CadenzaService.createMetaTask(
|
|
5370
|
-
"Sort tables by dependencies",
|
|
5371
|
-
this.sortTablesByReferences.bind(this),
|
|
5372
|
-
"Sorts tables by dependencies"
|
|
5373
|
-
).then(
|
|
5374
|
-
CadenzaService.createMetaTask(
|
|
5375
|
-
"Split schema into tables",
|
|
5376
|
-
this.splitTables.bind(this),
|
|
5377
|
-
"Generates DDL for database schema"
|
|
5378
|
-
).then(
|
|
5379
|
-
CadenzaService.createMetaTask(
|
|
5380
|
-
"Generate DDL from table",
|
|
5381
|
-
async (ctx) => {
|
|
5382
|
-
const {
|
|
5383
|
-
ddl,
|
|
5384
|
-
table,
|
|
5385
|
-
tableName,
|
|
5386
|
-
schema,
|
|
5387
|
-
options,
|
|
5388
|
-
sortedTables
|
|
5389
|
-
} = ctx;
|
|
5390
|
-
const fieldDefs = Object.entries(table.fields).map((value) => {
|
|
5391
|
-
const [fieldName, field] = value;
|
|
5392
|
-
let def = `${fieldName} ${field.type.toUpperCase()}`;
|
|
5393
|
-
if (field.type === "varchar")
|
|
5394
|
-
def += `(${field.constraints?.maxLength ?? 255})`;
|
|
5395
|
-
if (field.type === "decimal")
|
|
5396
|
-
def += `(${field.constraints?.precision ?? 10},${field.constraints?.scale ?? 2})`;
|
|
5397
|
-
if (field.primary) def += " PRIMARY KEY";
|
|
5398
|
-
if (field.unique) def += " UNIQUE";
|
|
5399
|
-
if (field.default !== void 0)
|
|
5400
|
-
def += ` DEFAULT ${field.default === "" ? "''" : String(field.default)}`;
|
|
5401
|
-
if (field.required && !field.nullable)
|
|
5402
|
-
def += " NOT NULL";
|
|
5403
|
-
if (field.nullable) def += " NULL";
|
|
5404
|
-
if (field.generated)
|
|
5405
|
-
def += ` GENERATED ALWAYS AS ${field.generated.toUpperCase()} STORED`;
|
|
5406
|
-
if (field.references)
|
|
5407
|
-
def += ` REFERENCES ${field.references} ON DELETE ${field.onDelete || "CASCADE"}`;
|
|
5408
|
-
if (field.encrypted) def += " ENCRYPTED";
|
|
5409
|
-
if (field.constraints?.check) {
|
|
5410
|
-
def += ` CHECK (${field.constraints.check})`;
|
|
5411
|
-
}
|
|
5412
|
-
return def;
|
|
5413
|
-
}).join(", ");
|
|
5414
|
-
if (schema.meta?.dropExisting) {
|
|
5415
|
-
const result = await this.dbClient.query(
|
|
5416
|
-
`DELETE FROM ${tableName};`
|
|
5417
|
-
);
|
|
5418
|
-
console.log("DROP TABLE", tableName, result);
|
|
5419
|
-
}
|
|
5420
|
-
const sql = `CREATE TABLE IF NOT EXISTS ${tableName} (${fieldDefs})`;
|
|
5421
|
-
ddl.push(sql);
|
|
5422
|
-
return {
|
|
5423
|
-
ddl,
|
|
5424
|
-
table,
|
|
5425
|
-
tableName,
|
|
5426
|
-
schema,
|
|
5427
|
-
options,
|
|
5428
|
-
sortedTables
|
|
5429
|
-
};
|
|
5430
|
-
}
|
|
5431
|
-
).then(
|
|
5432
|
-
CadenzaService.createMetaTask("Generate index DDL", (ctx) => {
|
|
5433
|
-
const {
|
|
5434
|
-
ddl,
|
|
5435
|
-
table,
|
|
5436
|
-
tableName,
|
|
5437
|
-
schema,
|
|
5438
|
-
options,
|
|
5439
|
-
sortedTables
|
|
5440
|
-
} = ctx;
|
|
5441
|
-
if (table.indexes) {
|
|
5442
|
-
table.indexes.forEach((fields) => {
|
|
5443
|
-
ddl.push(
|
|
5444
|
-
`CREATE INDEX IF NOT EXISTS idx_${tableName}_${fields.join("_")} ON ${tableName} (${fields.join(", ")});`
|
|
5445
|
-
);
|
|
5446
|
-
});
|
|
5447
|
-
}
|
|
5448
|
-
return {
|
|
5449
|
-
ddl,
|
|
5450
|
-
table,
|
|
5451
|
-
tableName,
|
|
5452
|
-
schema,
|
|
5453
|
-
options,
|
|
5454
|
-
sortedTables
|
|
5455
|
-
};
|
|
5456
|
-
}).then(
|
|
5457
|
-
CadenzaService.createMetaTask(
|
|
5458
|
-
"Generate primary key ddl",
|
|
5459
|
-
(ctx) => {
|
|
5460
|
-
const {
|
|
5461
|
-
ddl,
|
|
5462
|
-
table,
|
|
5463
|
-
tableName,
|
|
5464
|
-
schema,
|
|
5465
|
-
options,
|
|
5466
|
-
sortedTables
|
|
5467
|
-
} = ctx;
|
|
5468
|
-
if (table.primaryKey) {
|
|
5469
|
-
ddl.push(
|
|
5470
|
-
`ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS unique_${tableName}_${table.primaryKey.join("_")};`,
|
|
5471
|
-
// TODO: should be cascade?
|
|
5472
|
-
`ALTER TABLE ${tableName} ADD CONSTRAINT unique_${tableName}_${table.primaryKey.join("_")} PRIMARY KEY (${table.primaryKey.join(", ")});`
|
|
5473
|
-
);
|
|
5474
|
-
}
|
|
5475
|
-
return {
|
|
5476
|
-
ddl,
|
|
5477
|
-
table,
|
|
5478
|
-
tableName,
|
|
5479
|
-
schema,
|
|
5480
|
-
options,
|
|
5481
|
-
sortedTables
|
|
5482
|
-
};
|
|
5483
|
-
}
|
|
5484
|
-
).then(
|
|
5485
|
-
CadenzaService.createMetaTask(
|
|
5486
|
-
"Generate unique index DDL",
|
|
5487
|
-
(ctx) => {
|
|
5488
|
-
const {
|
|
5489
|
-
ddl,
|
|
5490
|
-
table,
|
|
5491
|
-
tableName,
|
|
5492
|
-
schema,
|
|
5493
|
-
options,
|
|
5494
|
-
sortedTables
|
|
5495
|
-
} = ctx;
|
|
5496
|
-
if (table.uniqueConstraints) {
|
|
5497
|
-
table.uniqueConstraints.forEach(
|
|
5498
|
-
(fields) => {
|
|
5499
|
-
ddl.push(
|
|
5500
|
-
`ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS unique_${tableName}_${fields.join("_")};`,
|
|
5501
|
-
// TODO: should be cascade?
|
|
5502
|
-
`ALTER TABLE ${tableName} ADD CONSTRAINT unique_${tableName}_${fields.join("_")} UNIQUE (${fields.join(", ")});`
|
|
5503
|
-
);
|
|
5504
|
-
}
|
|
5505
|
-
);
|
|
5506
|
-
}
|
|
5507
|
-
return {
|
|
5508
|
-
ddl,
|
|
5509
|
-
table,
|
|
5510
|
-
tableName,
|
|
5511
|
-
schema,
|
|
5512
|
-
options,
|
|
5513
|
-
sortedTables
|
|
5514
|
-
};
|
|
5515
|
-
}
|
|
5516
|
-
).then(
|
|
5517
|
-
CadenzaService.createMetaTask(
|
|
5518
|
-
"Generate foreign key DDL",
|
|
5519
|
-
(ctx) => {
|
|
5520
|
-
const {
|
|
5521
|
-
ddl,
|
|
5522
|
-
table,
|
|
5523
|
-
tableName,
|
|
5524
|
-
schema,
|
|
5525
|
-
options,
|
|
5526
|
-
sortedTables
|
|
5527
|
-
} = ctx;
|
|
5528
|
-
if (table.foreignKeys) {
|
|
5529
|
-
for (const foreignKey of table.foreignKeys) {
|
|
5530
|
-
const foreignKeyName = `fk_${tableName}_${foreignKey.fields.join("_")}`;
|
|
5531
|
-
ddl.push(
|
|
5532
|
-
`ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS ${foreignKeyName};`,
|
|
5533
|
-
// TODO: should be cascade?
|
|
5534
|
-
`ALTER TABLE ${tableName} ADD CONSTRAINT ${foreignKeyName} FOREIGN KEY (${foreignKey.fields.join(
|
|
5535
|
-
", "
|
|
5536
|
-
)}) REFERENCES ${foreignKey.tableName} (${foreignKey.referenceFields.join(
|
|
5537
|
-
", "
|
|
5538
|
-
)});`
|
|
5539
|
-
);
|
|
5540
|
-
}
|
|
5541
|
-
}
|
|
5542
|
-
return {
|
|
5543
|
-
ddl,
|
|
5544
|
-
table,
|
|
5545
|
-
tableName,
|
|
5546
|
-
schema,
|
|
5547
|
-
options,
|
|
5548
|
-
sortedTables
|
|
5549
|
-
};
|
|
5550
|
-
}
|
|
5551
|
-
).then(
|
|
5552
|
-
CadenzaService.createMetaTask(
|
|
5553
|
-
"Generate trigger DDL",
|
|
5554
|
-
(ctx) => {
|
|
5555
|
-
const {
|
|
5556
|
-
ddl,
|
|
5557
|
-
table,
|
|
5558
|
-
tableName,
|
|
5559
|
-
schema,
|
|
5560
|
-
options,
|
|
5561
|
-
sortedTables
|
|
5562
|
-
} = ctx;
|
|
5563
|
-
if (table.triggers) {
|
|
5564
|
-
for (const [
|
|
5565
|
-
triggerName,
|
|
5566
|
-
trigger
|
|
5567
|
-
] of Object.entries(table.triggers)) {
|
|
5568
|
-
ddl.push(
|
|
5569
|
-
`CREATE TRIGGER ${triggerName} ${trigger.when} ${trigger.event} ON ${tableName} FOR EACH STATEMENT EXECUTE FUNCTION ${trigger.function};`
|
|
5570
|
-
);
|
|
5571
|
-
}
|
|
5572
|
-
}
|
|
5573
|
-
return {
|
|
5574
|
-
ddl,
|
|
5575
|
-
table,
|
|
5576
|
-
tableName,
|
|
5577
|
-
schema,
|
|
5578
|
-
options,
|
|
5579
|
-
sortedTables
|
|
5580
|
-
};
|
|
5581
|
-
}
|
|
5582
|
-
).then(
|
|
5583
|
-
CadenzaService.createMetaTask(
|
|
5584
|
-
"Generate initial data DDL",
|
|
5585
|
-
(ctx) => {
|
|
5586
|
-
const {
|
|
5587
|
-
ddl,
|
|
5588
|
-
table,
|
|
5589
|
-
tableName,
|
|
5590
|
-
schema,
|
|
5591
|
-
options,
|
|
5592
|
-
sortedTables
|
|
5593
|
-
} = ctx;
|
|
5594
|
-
if (table.initialData) {
|
|
5595
|
-
ddl.push(
|
|
5596
|
-
`INSERT INTO ${tableName} (${table.initialData.fields.join(", ")}) VALUES ${table.initialData.data.map(
|
|
5597
|
-
(row) => `(${row.map(
|
|
5598
|
-
(value) => value === void 0 ? "NULL" : value.charAt(0) === "'" ? value : `'${value}'`
|
|
5599
|
-
).join(", ")})`
|
|
5600
|
-
// TODO: handle non string data
|
|
5601
|
-
).join(", ")} ON CONFLICT DO NOTHING;`
|
|
5602
|
-
);
|
|
5603
|
-
}
|
|
5604
|
-
return {
|
|
5605
|
-
ddl,
|
|
5606
|
-
table,
|
|
5607
|
-
tableName,
|
|
5608
|
-
schema,
|
|
5609
|
-
options,
|
|
5610
|
-
sortedTables
|
|
5611
|
-
};
|
|
5612
|
-
}
|
|
5613
|
-
).then(
|
|
5614
|
-
CadenzaService.createUniqueMetaTask(
|
|
5615
|
-
"Join DDL",
|
|
5616
|
-
(ctx) => {
|
|
5617
|
-
const { joinedContexts } = ctx;
|
|
5618
|
-
const ddl = [];
|
|
5619
|
-
for (const joinedContext of joinedContexts) {
|
|
5620
|
-
ddl.push(...joinedContext.ddl);
|
|
5621
|
-
}
|
|
5622
|
-
ddl.flat();
|
|
5623
|
-
return {
|
|
5624
|
-
ddl,
|
|
5625
|
-
schema: joinedContexts[0].schema,
|
|
5626
|
-
options: joinedContexts[0].options,
|
|
5627
|
-
table: joinedContexts[0].table,
|
|
5628
|
-
tableName: joinedContexts[0].tableName,
|
|
5629
|
-
sortedTables: joinedContexts[0].sortedTables
|
|
5630
|
-
};
|
|
5631
|
-
}
|
|
5632
|
-
).then(
|
|
5633
|
-
CadenzaService.createMetaTask(
|
|
5634
|
-
"Apply Database Changes",
|
|
5635
|
-
async (ctx) => {
|
|
5636
|
-
const { ddl } = ctx;
|
|
5637
|
-
if (ddl && ddl.length > 0) {
|
|
5638
|
-
for (const sql of ddl) {
|
|
5639
|
-
try {
|
|
5640
|
-
await this.dbClient.query(sql);
|
|
5641
|
-
} catch (error) {
|
|
5642
|
-
console.error(
|
|
5643
|
-
"Error applying DDL",
|
|
5644
|
-
sql,
|
|
5645
|
-
error
|
|
5646
|
-
);
|
|
5647
|
-
}
|
|
5648
|
-
}
|
|
5649
|
-
}
|
|
5650
|
-
return true;
|
|
5651
|
-
},
|
|
5652
|
-
"Applies generated DDL to the database"
|
|
5653
|
-
).then(
|
|
5654
|
-
CadenzaService.createMetaTask(
|
|
5655
|
-
"Split schema into tables for task creation",
|
|
5656
|
-
this.splitTables.bind(this),
|
|
5657
|
-
"Splits schema into tables for task creation"
|
|
5658
|
-
).then(
|
|
5659
|
-
CadenzaService.createMetaTask(
|
|
5660
|
-
"Generate tasks",
|
|
5661
|
-
(ctx) => {
|
|
5662
|
-
const { table, tableName, options } = ctx;
|
|
5663
|
-
this.createDatabaseTask(
|
|
5664
|
-
"query",
|
|
5665
|
-
tableName,
|
|
5666
|
-
table,
|
|
5667
|
-
this.queryFunction.bind(this),
|
|
5668
|
-
options
|
|
5669
|
-
);
|
|
5670
|
-
this.createDatabaseTask(
|
|
5671
|
-
"insert",
|
|
5672
|
-
tableName,
|
|
5673
|
-
table,
|
|
5674
|
-
this.insertFunction.bind(this),
|
|
5675
|
-
options
|
|
5676
|
-
);
|
|
5677
|
-
this.createDatabaseTask(
|
|
5678
|
-
"update",
|
|
5679
|
-
tableName,
|
|
5680
|
-
table,
|
|
5681
|
-
this.updateFunction.bind(this),
|
|
5682
|
-
options
|
|
5683
|
-
);
|
|
5684
|
-
this.createDatabaseTask(
|
|
5685
|
-
"delete",
|
|
5686
|
-
tableName,
|
|
5687
|
-
table,
|
|
5688
|
-
this.deleteFunction.bind(this),
|
|
5689
|
-
options
|
|
5690
|
-
);
|
|
5691
|
-
return true;
|
|
5692
|
-
},
|
|
5693
|
-
"Generates auto-tasks for database schema"
|
|
5694
|
-
).then(
|
|
5695
|
-
CadenzaService.createUniqueMetaTask(
|
|
5696
|
-
"Join table tasks",
|
|
5697
|
-
() => {
|
|
5698
|
-
return true;
|
|
5699
|
-
}
|
|
5700
|
-
).emits("meta.database.setup_done")
|
|
5701
|
-
)
|
|
5702
|
-
)
|
|
5703
|
-
)
|
|
5704
|
-
)
|
|
5705
|
-
)
|
|
5706
|
-
)
|
|
5707
|
-
)
|
|
5708
|
-
)
|
|
5709
|
-
)
|
|
5710
|
-
)
|
|
5711
|
-
)
|
|
5712
|
-
)
|
|
5713
|
-
)
|
|
5714
|
-
)
|
|
5715
|
-
)
|
|
5716
|
-
],
|
|
5717
|
-
"Initializes the database service with schema parsing and task/signal generation"
|
|
5347
|
+
CadenzaService.createMetaTask(
|
|
5348
|
+
"Route PostgresActor setup requests",
|
|
5349
|
+
(ctx) => {
|
|
5350
|
+
const registration = this.resolveRegistration(ctx);
|
|
5351
|
+
if (!registration) {
|
|
5352
|
+
return ctx;
|
|
5353
|
+
}
|
|
5354
|
+
this.requestPostgresActorSetup(registration, ctx);
|
|
5355
|
+
return ctx;
|
|
5356
|
+
},
|
|
5357
|
+
"Routes generic database init requests to actor-scoped setup signal.",
|
|
5358
|
+
{ isMeta: true, isSubMeta: true }
|
|
5718
5359
|
).doOn("meta.database_init_requested");
|
|
5719
5360
|
}
|
|
5720
5361
|
static get instance() {
|
|
@@ -5722,632 +5363,1093 @@ var DatabaseController = class _DatabaseController {
|
|
|
5722
5363
|
return this._instance;
|
|
5723
5364
|
}
|
|
5724
5365
|
reset() {
|
|
5725
|
-
this.
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
* The method modifies the client instance by adding timeout tracking and logging functionality to ensure
|
|
5730
|
-
* the client is not held for an extended period and track the last executed query for debugging purposes.
|
|
5731
|
-
*
|
|
5732
|
-
* @return {Promise<PoolClient>} A promise resolving to a database client from the pool with enhanced behavior for query tracking and timeout handling.
|
|
5733
|
-
*/
|
|
5734
|
-
async getClient() {
|
|
5735
|
-
const client = await this.dbClient.connect();
|
|
5736
|
-
const query = client.query;
|
|
5737
|
-
const release = client.release;
|
|
5738
|
-
const timeout = setTimeout(() => {
|
|
5739
|
-
CadenzaService.log(
|
|
5740
|
-
"CRITICAL: A database client has been checked out for more than 5 seconds!",
|
|
5741
|
-
{
|
|
5742
|
-
clientId: client.uuid,
|
|
5743
|
-
query: client.lastQuery,
|
|
5744
|
-
databaseName: this.databaseName
|
|
5745
|
-
},
|
|
5746
|
-
"critical"
|
|
5747
|
-
);
|
|
5748
|
-
}, 5e3);
|
|
5749
|
-
client.query = (...args) => {
|
|
5750
|
-
client.lastQuery = args;
|
|
5751
|
-
return query.apply(client, args);
|
|
5752
|
-
};
|
|
5753
|
-
client.release = () => {
|
|
5754
|
-
clearTimeout(timeout);
|
|
5755
|
-
client.query = query;
|
|
5756
|
-
client.release = release;
|
|
5757
|
-
return release.apply(client);
|
|
5758
|
-
};
|
|
5759
|
-
return client;
|
|
5760
|
-
}
|
|
5761
|
-
async waitForDatabase(transaction, client, context) {
|
|
5762
|
-
for (let i = 0; i < 10; i++) {
|
|
5763
|
-
try {
|
|
5764
|
-
return await transaction(client, context);
|
|
5765
|
-
} catch (err) {
|
|
5766
|
-
if (err && err.message.includes("does not exist")) {
|
|
5767
|
-
CadenzaService.log("Waiting for database to be ready...");
|
|
5768
|
-
await new Promise((res) => setTimeout(res, 1e3));
|
|
5769
|
-
} else {
|
|
5770
|
-
CadenzaService.log(
|
|
5771
|
-
"Database query errored",
|
|
5772
|
-
{ error: err, context },
|
|
5773
|
-
"warning"
|
|
5774
|
-
);
|
|
5775
|
-
return { rows: [] };
|
|
5776
|
-
}
|
|
5366
|
+
for (const registration of this.registrationsByActorName.values()) {
|
|
5367
|
+
const runtimeState = registration.actor.getRuntimeState(registration.actorKey);
|
|
5368
|
+
if (runtimeState?.pool) {
|
|
5369
|
+
runtimeState.pool.end().catch(() => void 0);
|
|
5777
5370
|
}
|
|
5778
5371
|
}
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5372
|
+
this.registrationsByActorName.clear();
|
|
5373
|
+
this.registrationsByActorToken.clear();
|
|
5374
|
+
this.adminDbClient.end().catch(() => void 0);
|
|
5375
|
+
}
|
|
5376
|
+
createPostgresActor(name, schema, description, options) {
|
|
5377
|
+
const actorName = buildPostgresActorName(name);
|
|
5378
|
+
const existing = this.registrationsByActorName.get(actorName);
|
|
5379
|
+
if (existing) {
|
|
5380
|
+
return existing;
|
|
5381
|
+
}
|
|
5382
|
+
const actorToken = normalizeIntentToken(actorName);
|
|
5383
|
+
const actorKey = String(options.databaseName ?? snakeCase(name));
|
|
5384
|
+
const ownerServiceName = options.ownerServiceName ?? CadenzaService.serviceRegistry?.serviceName ?? null;
|
|
5385
|
+
const optionTimeout = typeof options.timeoutMs === "number" ? Number(options.timeoutMs) : Number(options.timeout);
|
|
5386
|
+
const safetyPolicy = {
|
|
5387
|
+
statementTimeoutMs: normalizePositiveInteger(
|
|
5388
|
+
optionTimeout,
|
|
5389
|
+
normalizePositiveInteger(Number(process.env.DATABASE_STATEMENT_TIMEOUT_MS ?? 15e3), 15e3)
|
|
5390
|
+
),
|
|
5391
|
+
retryCount: normalizePositiveInteger(options.retryCount, 3, 0),
|
|
5392
|
+
retryDelayMs: normalizePositiveInteger(Number(process.env.DATABASE_RETRY_DELAY_MS ?? 100), 100),
|
|
5393
|
+
retryDelayMaxMs: normalizePositiveInteger(
|
|
5394
|
+
Number(process.env.DATABASE_RETRY_DELAY_MAX_MS ?? 1e3),
|
|
5395
|
+
1e3
|
|
5396
|
+
),
|
|
5397
|
+
retryDelayFactor: Number(process.env.DATABASE_RETRY_DELAY_FACTOR ?? 2)
|
|
5398
|
+
};
|
|
5399
|
+
const actor = CadenzaService.createActor(
|
|
5400
|
+
{
|
|
5401
|
+
name: actorName,
|
|
5402
|
+
description: description || "Specialized PostgresActor owning pool runtime state and schema-driven DB task generation.",
|
|
5403
|
+
defaultKey: actorKey,
|
|
5404
|
+
keyResolver: (input) => typeof input.databaseName === "string" ? input.databaseName : void 0,
|
|
5405
|
+
loadPolicy: "eager",
|
|
5406
|
+
writeContract: "overwrite",
|
|
5407
|
+
initState: {
|
|
5408
|
+
actorName,
|
|
5409
|
+
actorToken,
|
|
5410
|
+
ownerServiceName,
|
|
5411
|
+
databaseName: actorKey,
|
|
5412
|
+
status: "idle",
|
|
5413
|
+
schemaVersion: Number(schema.version ?? 1),
|
|
5414
|
+
setupStartedAt: null,
|
|
5415
|
+
setupCompletedAt: null,
|
|
5416
|
+
lastHealthCheckAt: null,
|
|
5417
|
+
lastError: null,
|
|
5418
|
+
tables: Object.keys(schema.tables ?? {}),
|
|
5419
|
+
safetyPolicy
|
|
5818
5420
|
}
|
|
5819
|
-
}
|
|
5421
|
+
},
|
|
5422
|
+
{ isMeta: Boolean(options.isMeta) }
|
|
5423
|
+
);
|
|
5424
|
+
const registration = {
|
|
5425
|
+
ownerServiceName,
|
|
5426
|
+
databaseName: actorKey,
|
|
5427
|
+
actorName,
|
|
5428
|
+
actorToken,
|
|
5429
|
+
actorKey,
|
|
5430
|
+
setupSignal: `meta.postgres_actor.setup_requested.${actorToken}`,
|
|
5431
|
+
setupDoneSignal: `meta.postgres_actor.setup_done.${actorToken}`,
|
|
5432
|
+
setupFailedSignal: `meta.postgres_actor.setup_failed.${actorToken}`,
|
|
5433
|
+
actor,
|
|
5434
|
+
schema,
|
|
5435
|
+
description,
|
|
5436
|
+
options,
|
|
5437
|
+
tasksGenerated: false,
|
|
5438
|
+
intentNames: /* @__PURE__ */ new Set()
|
|
5439
|
+
};
|
|
5440
|
+
this.registrationsByActorName.set(actorName, registration);
|
|
5441
|
+
this.registrationsByActorToken.set(actorToken, registration);
|
|
5442
|
+
this.registerSetupTask(registration);
|
|
5443
|
+
return registration;
|
|
5444
|
+
}
|
|
5445
|
+
requestPostgresActorSetup(registrationOrName, ctx = {}) {
|
|
5446
|
+
const registration = typeof registrationOrName === "string" ? this.resolveRegistration({
|
|
5447
|
+
actorName: registrationOrName
|
|
5448
|
+
}) : registrationOrName;
|
|
5449
|
+
if (!registration) {
|
|
5450
|
+
return void 0;
|
|
5820
5451
|
}
|
|
5821
|
-
const
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
visit(dep);
|
|
5452
|
+
const payload = {
|
|
5453
|
+
...ctx,
|
|
5454
|
+
actorName: registration.actorName,
|
|
5455
|
+
actorToken: registration.actorToken,
|
|
5456
|
+
databaseName: registration.databaseName,
|
|
5457
|
+
ownerServiceName: registration.ownerServiceName,
|
|
5458
|
+
options: {
|
|
5459
|
+
...ctx.options ?? {},
|
|
5460
|
+
actorName: registration.actorName,
|
|
5461
|
+
actorToken: registration.actorToken,
|
|
5462
|
+
ownerServiceName: registration.ownerServiceName,
|
|
5463
|
+
databaseName: registration.databaseName
|
|
5834
5464
|
}
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5465
|
+
};
|
|
5466
|
+
const runtimeState = registration.actor.getRuntimeState(registration.actorKey);
|
|
5467
|
+
if (runtimeState?.ready) {
|
|
5468
|
+
this.emitSetupDone(registration, payload);
|
|
5469
|
+
return registration;
|
|
5838
5470
|
}
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5471
|
+
CadenzaService.emit(registration.setupSignal, payload);
|
|
5472
|
+
return registration;
|
|
5473
|
+
}
|
|
5474
|
+
resolveRegistration(ctx) {
|
|
5475
|
+
const rawActorToken = String(
|
|
5476
|
+
ctx.options?.actorToken ?? ctx.actorToken ?? ""
|
|
5477
|
+
).trim();
|
|
5478
|
+
if (rawActorToken) {
|
|
5479
|
+
const actorToken = normalizeIntentToken(rawActorToken);
|
|
5480
|
+
const registration = this.registrationsByActorToken.get(actorToken);
|
|
5481
|
+
if (registration) {
|
|
5482
|
+
return registration;
|
|
5842
5483
|
}
|
|
5843
5484
|
}
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5485
|
+
const rawActorName = String(
|
|
5486
|
+
ctx.options?.actorName ?? ctx.actorName ?? ctx.options?.postgresActorName ?? ctx.postgresActorName ?? ""
|
|
5487
|
+
).trim();
|
|
5488
|
+
if (rawActorName) {
|
|
5489
|
+
const registration = this.registrationsByActorName.get(rawActorName) ?? this.registrationsByActorName.get(buildPostgresActorName(rawActorName));
|
|
5490
|
+
if (registration) {
|
|
5491
|
+
return registration;
|
|
5847
5492
|
}
|
|
5848
5493
|
}
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
* @param {string[]} ctx.sortedTables - An array of table names sorted in a specific order.
|
|
5857
|
-
* @param {Object} ctx.schema - The schema object that includes table definitions.
|
|
5858
|
-
* @param {Object} [ctx.options={}] - Optional configuration options for processing tables.
|
|
5859
|
-
*
|
|
5860
|
-
* @return {AsyncGenerator} An asynchronous generator that yields objects containing the table definition, metadata, and other context details.
|
|
5861
|
-
*/
|
|
5862
|
-
async *splitTables(ctx) {
|
|
5863
|
-
const { sortedTables, schema, options = {} } = ctx;
|
|
5864
|
-
for (const tableName of sortedTables) {
|
|
5865
|
-
const table = schema.tables[tableName];
|
|
5866
|
-
yield { ddl: [], table, tableName, schema, options, sortedTables };
|
|
5494
|
+
const legacyServiceName = String(
|
|
5495
|
+
ctx.options?.serviceName ?? ctx.serviceName ?? ""
|
|
5496
|
+
).trim();
|
|
5497
|
+
if (legacyServiceName) {
|
|
5498
|
+
return this.registrationsByActorName.get(
|
|
5499
|
+
buildPostgresActorName(legacyServiceName)
|
|
5500
|
+
);
|
|
5867
5501
|
}
|
|
5502
|
+
return void 0;
|
|
5868
5503
|
}
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
return camelCasedRow;
|
|
5882
|
-
});
|
|
5504
|
+
emitSetupDone(registration, payload) {
|
|
5505
|
+
const resolvedPayload = {
|
|
5506
|
+
...payload,
|
|
5507
|
+
actorName: registration.actorName,
|
|
5508
|
+
actorToken: registration.actorToken,
|
|
5509
|
+
databaseName: registration.databaseName,
|
|
5510
|
+
ownerServiceName: registration.ownerServiceName,
|
|
5511
|
+
__success: true
|
|
5512
|
+
};
|
|
5513
|
+
CadenzaService.emit(registration.setupDoneSignal, resolvedPayload);
|
|
5514
|
+
CadenzaService.emit("meta.postgres_actor.setup_done", resolvedPayload);
|
|
5515
|
+
CadenzaService.emit("meta.database.setup_done", resolvedPayload);
|
|
5883
5516
|
}
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5517
|
+
registerSetupTask(registration) {
|
|
5518
|
+
CadenzaService.createMetaTask(
|
|
5519
|
+
`Setup ${registration.actorName}`,
|
|
5520
|
+
registration.actor.task(
|
|
5521
|
+
async ({ input, state, runtimeState, setState, setRuntimeState, emit }) => {
|
|
5522
|
+
const requestedDatabaseName = String(
|
|
5523
|
+
input.options?.databaseName ?? input.databaseName ?? registration.databaseName
|
|
5524
|
+
);
|
|
5525
|
+
if (requestedDatabaseName !== registration.databaseName) {
|
|
5526
|
+
return input;
|
|
5527
|
+
}
|
|
5528
|
+
setState({
|
|
5529
|
+
...state,
|
|
5530
|
+
status: "initializing",
|
|
5531
|
+
setupStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5532
|
+
lastError: null
|
|
5533
|
+
});
|
|
5534
|
+
const priorRuntimePool = runtimeState?.pool ?? null;
|
|
5535
|
+
if (priorRuntimePool) {
|
|
5536
|
+
await priorRuntimePool.end().catch(() => void 0);
|
|
5537
|
+
}
|
|
5538
|
+
try {
|
|
5539
|
+
await this.createDatabaseIfMissing(requestedDatabaseName);
|
|
5540
|
+
const pool = this.createTargetPool(
|
|
5541
|
+
requestedDatabaseName,
|
|
5542
|
+
state.safetyPolicy.statementTimeoutMs
|
|
5543
|
+
);
|
|
5544
|
+
await this.checkPoolHealth(pool, state.safetyPolicy);
|
|
5545
|
+
this.validateSchema({
|
|
5546
|
+
schema: registration.schema,
|
|
5547
|
+
options: registration.options
|
|
5548
|
+
});
|
|
5549
|
+
const sortedTables = this.sortTablesByReferences({
|
|
5550
|
+
schema: registration.schema
|
|
5551
|
+
}).sortedTables;
|
|
5552
|
+
const ddlStatements = this.buildSchemaDdlStatements(
|
|
5553
|
+
registration.schema,
|
|
5554
|
+
sortedTables
|
|
5555
|
+
);
|
|
5556
|
+
await this.applyDdlStatements(pool, ddlStatements);
|
|
5557
|
+
if (!registration.tasksGenerated) {
|
|
5558
|
+
this.generateDatabaseTasks(registration);
|
|
5559
|
+
registration.tasksGenerated = true;
|
|
5560
|
+
}
|
|
5561
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
5562
|
+
setRuntimeState({
|
|
5563
|
+
pool,
|
|
5564
|
+
ready: true,
|
|
5565
|
+
pendingQueries: 0,
|
|
5566
|
+
lastHealthCheckAt: Date.now(),
|
|
5567
|
+
lastError: null
|
|
5568
|
+
});
|
|
5569
|
+
setState({
|
|
5570
|
+
...state,
|
|
5571
|
+
status: "ready",
|
|
5572
|
+
setupCompletedAt: nowIso,
|
|
5573
|
+
lastHealthCheckAt: nowIso,
|
|
5574
|
+
lastError: null,
|
|
5575
|
+
tables: Object.keys(registration.schema.tables ?? {})
|
|
5576
|
+
});
|
|
5577
|
+
this.emitSetupDone(registration, {
|
|
5578
|
+
...input
|
|
5579
|
+
});
|
|
5580
|
+
return {
|
|
5581
|
+
...input,
|
|
5582
|
+
__success: true,
|
|
5583
|
+
actorName: registration.actorName,
|
|
5584
|
+
databaseName: registration.databaseName,
|
|
5585
|
+
ownerServiceName: registration.ownerServiceName
|
|
5586
|
+
};
|
|
5587
|
+
} catch (error) {
|
|
5588
|
+
const message = errorMessage(error);
|
|
5589
|
+
setRuntimeState({
|
|
5590
|
+
pool: null,
|
|
5591
|
+
ready: false,
|
|
5592
|
+
pendingQueries: 0,
|
|
5593
|
+
lastHealthCheckAt: runtimeState?.lastHealthCheckAt ?? null,
|
|
5594
|
+
lastError: message
|
|
5595
|
+
});
|
|
5596
|
+
setState({
|
|
5597
|
+
...state,
|
|
5598
|
+
status: "error",
|
|
5599
|
+
setupCompletedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5600
|
+
lastError: message
|
|
5601
|
+
});
|
|
5602
|
+
throw error;
|
|
5603
|
+
}
|
|
5604
|
+
},
|
|
5605
|
+
{ mode: "write" }
|
|
5606
|
+
),
|
|
5607
|
+
"Initializes PostgresActor runtime pool, applies schema, and generates CRUD tasks/intents.",
|
|
5608
|
+
{ isMeta: true }
|
|
5609
|
+
).doOn(registration.setupSignal).emitsOnFail(
|
|
5610
|
+
registration.setupFailedSignal,
|
|
5611
|
+
"meta.postgres_actor.setup_failed",
|
|
5612
|
+
"meta.database.setup_failed"
|
|
5613
|
+
);
|
|
5614
|
+
}
|
|
5615
|
+
createTargetPool(databaseName, statementTimeoutMs) {
|
|
5616
|
+
const connectionString = this.buildDatabaseConnectionString(databaseName);
|
|
5617
|
+
return new Pool({
|
|
5618
|
+
connectionString,
|
|
5619
|
+
statement_timeout: statementTimeoutMs,
|
|
5620
|
+
query_timeout: statementTimeoutMs,
|
|
5621
|
+
ssl: {
|
|
5622
|
+
rejectUnauthorized: false
|
|
5623
|
+
}
|
|
5624
|
+
});
|
|
5625
|
+
}
|
|
5626
|
+
buildDatabaseConnectionString(databaseName) {
|
|
5627
|
+
const base = process.env.DATABASE_ADDRESS ?? "";
|
|
5628
|
+
if (!base) {
|
|
5629
|
+
throw new Error("DATABASE_ADDRESS environment variable is required");
|
|
5630
|
+
}
|
|
5631
|
+
try {
|
|
5632
|
+
const parsed = new URL(base);
|
|
5633
|
+
parsed.pathname = `/${databaseName}`;
|
|
5634
|
+
if (!parsed.searchParams.has("sslmode")) {
|
|
5635
|
+
parsed.searchParams.set("sslmode", "disable");
|
|
5636
|
+
}
|
|
5637
|
+
return parsed.toString();
|
|
5638
|
+
} catch {
|
|
5639
|
+
const lastSlashIndex = base.lastIndexOf("/");
|
|
5640
|
+
if (lastSlashIndex === -1) {
|
|
5641
|
+
throw new Error("DATABASE_ADDRESS must be a valid postgres connection string");
|
|
5642
|
+
}
|
|
5643
|
+
const root = base.slice(0, lastSlashIndex + 1);
|
|
5644
|
+
return `${root}${databaseName}?sslmode=disable`;
|
|
5645
|
+
}
|
|
5646
|
+
}
|
|
5647
|
+
async createDatabaseIfMissing(databaseName) {
|
|
5648
|
+
if (!isSqlIdentifier(databaseName)) {
|
|
5649
|
+
throw new Error(
|
|
5650
|
+
`Invalid database name '${databaseName}'. Names must contain only lowercase alphanumerics and underscores and cannot start with a number.`
|
|
5651
|
+
);
|
|
5652
|
+
}
|
|
5653
|
+
try {
|
|
5654
|
+
await this.adminDbClient.query(`CREATE DATABASE ${databaseName}`);
|
|
5655
|
+
CadenzaService.log(`Database ${databaseName} created.`);
|
|
5656
|
+
} catch (error) {
|
|
5657
|
+
if (error?.code === "42P04") {
|
|
5658
|
+
CadenzaService.log(`Database ${databaseName} already exists.`);
|
|
5659
|
+
return;
|
|
5660
|
+
}
|
|
5661
|
+
throw new Error(`Failed to create database '${databaseName}': ${errorMessage(error)}`);
|
|
5662
|
+
}
|
|
5663
|
+
}
|
|
5664
|
+
async checkPoolHealth(pool, safetyPolicy) {
|
|
5665
|
+
await this.runWithRetries(
|
|
5666
|
+
async () => {
|
|
5667
|
+
await this.withTimeout(
|
|
5668
|
+
() => pool.query("SELECT 1 as health"),
|
|
5669
|
+
safetyPolicy.statementTimeoutMs,
|
|
5670
|
+
"Database health check timed out"
|
|
5671
|
+
);
|
|
5672
|
+
},
|
|
5673
|
+
safetyPolicy,
|
|
5674
|
+
"Health check"
|
|
5675
|
+
);
|
|
5676
|
+
}
|
|
5677
|
+
getPoolOrThrow(registration) {
|
|
5678
|
+
const runtimeState = registration.actor.getRuntimeState(
|
|
5679
|
+
registration.actorKey
|
|
5680
|
+
);
|
|
5681
|
+
if (!runtimeState || !runtimeState.ready || !runtimeState.pool) {
|
|
5682
|
+
throw new Error(
|
|
5683
|
+
`PostgresActor '${registration.actorName}' is not ready. Ensure setup completed before running DB tasks.`
|
|
5684
|
+
);
|
|
5685
|
+
}
|
|
5686
|
+
return runtimeState.pool;
|
|
5687
|
+
}
|
|
5688
|
+
async withTimeout(work, timeoutMs, timeoutMessage) {
|
|
5689
|
+
let timeoutHandle = null;
|
|
5690
|
+
try {
|
|
5691
|
+
return await Promise.race([
|
|
5692
|
+
work(),
|
|
5693
|
+
new Promise((_, reject) => {
|
|
5694
|
+
timeoutHandle = setTimeout(() => {
|
|
5695
|
+
reject(new Error(timeoutMessage));
|
|
5696
|
+
}, timeoutMs);
|
|
5697
|
+
})
|
|
5698
|
+
]);
|
|
5699
|
+
} finally {
|
|
5700
|
+
if (timeoutHandle) {
|
|
5701
|
+
clearTimeout(timeoutHandle);
|
|
5702
|
+
}
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5705
|
+
async runWithRetries(work, safetyPolicy, operationLabel) {
|
|
5706
|
+
const attempts = normalizePositiveInteger(safetyPolicy.retryCount, 0, 0) + 1;
|
|
5707
|
+
let delayMs = normalizePositiveInteger(safetyPolicy.retryDelayMs, 100);
|
|
5708
|
+
const maxDelayMs = normalizePositiveInteger(safetyPolicy.retryDelayMaxMs, 1e3);
|
|
5709
|
+
const factor = Number.isFinite(safetyPolicy.retryDelayFactor) ? Math.max(1, safetyPolicy.retryDelayFactor) : 1;
|
|
5710
|
+
let lastError;
|
|
5711
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
5712
|
+
try {
|
|
5713
|
+
return await work();
|
|
5714
|
+
} catch (error) {
|
|
5715
|
+
lastError = error;
|
|
5716
|
+
const transient = isTransientDatabaseError(error);
|
|
5717
|
+
if (!transient || attempt >= attempts) {
|
|
5718
|
+
break;
|
|
5719
|
+
}
|
|
5720
|
+
CadenzaService.log(
|
|
5721
|
+
`${operationLabel} failed with transient error. Retrying...`,
|
|
5722
|
+
{
|
|
5723
|
+
attempt,
|
|
5724
|
+
attempts,
|
|
5725
|
+
delayMs,
|
|
5726
|
+
error: errorMessage(error)
|
|
5727
|
+
},
|
|
5728
|
+
"warning"
|
|
5729
|
+
);
|
|
5730
|
+
await sleep(delayMs);
|
|
5731
|
+
delayMs = Math.min(Math.trunc(delayMs * factor), maxDelayMs);
|
|
5732
|
+
}
|
|
5733
|
+
}
|
|
5734
|
+
throw lastError;
|
|
5735
|
+
}
|
|
5736
|
+
async executeWithTransaction(pool, transaction, callback) {
|
|
5737
|
+
if (!transaction) {
|
|
5738
|
+
return callback(pool);
|
|
5739
|
+
}
|
|
5740
|
+
const client = await pool.connect();
|
|
5741
|
+
try {
|
|
5742
|
+
await client.query("BEGIN");
|
|
5743
|
+
const result = await callback(client);
|
|
5744
|
+
await client.query("COMMIT");
|
|
5745
|
+
return result;
|
|
5746
|
+
} catch (error) {
|
|
5747
|
+
await client.query("ROLLBACK");
|
|
5748
|
+
throw error;
|
|
5749
|
+
} finally {
|
|
5750
|
+
client.release();
|
|
5751
|
+
}
|
|
5752
|
+
}
|
|
5753
|
+
async queryFunction(registration, tableName, context) {
|
|
5754
|
+
const {
|
|
5755
|
+
filter = {},
|
|
5756
|
+
fields = [],
|
|
5757
|
+
joins = {},
|
|
5758
|
+
sort = {},
|
|
5759
|
+
limit,
|
|
5760
|
+
offset,
|
|
5761
|
+
queryMode = "rows",
|
|
5762
|
+
aggregates = [],
|
|
5763
|
+
groupBy = []
|
|
5899
5764
|
} = context;
|
|
5900
|
-
|
|
5765
|
+
const pool = this.getPoolOrThrow(registration);
|
|
5766
|
+
const statementTimeoutMs = this.resolveSafetyPolicy(registration).statementTimeoutMs;
|
|
5767
|
+
const resolvedMode = queryMode;
|
|
5768
|
+
const aggregateDefinitions = Array.isArray(aggregates) ? aggregates : [];
|
|
5769
|
+
const groupByFields = Array.isArray(groupBy) ? groupBy : [];
|
|
5901
5770
|
const params = [];
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
if (
|
|
5906
|
-
sql
|
|
5771
|
+
const whereClause = Object.keys(filter).length > 0 ? this.buildWhereClause(filter, params) : "";
|
|
5772
|
+
const joinClause = Object.keys(joins).length > 0 ? this.buildJoinClause(joins) : "";
|
|
5773
|
+
let sql;
|
|
5774
|
+
if (resolvedMode === "count") {
|
|
5775
|
+
sql = `SELECT COUNT(*)::bigint AS count FROM ${tableName} ${joinClause} ${whereClause}`;
|
|
5776
|
+
} else if (resolvedMode === "exists") {
|
|
5777
|
+
sql = `SELECT EXISTS(SELECT 1 FROM ${tableName} ${joinClause} ${whereClause}) AS exists`;
|
|
5778
|
+
} else if (resolvedMode === "aggregate") {
|
|
5779
|
+
if (aggregateDefinitions.length === 0) {
|
|
5780
|
+
throw new Error("Aggregate queries require at least one aggregate definition");
|
|
5781
|
+
}
|
|
5782
|
+
const aggregateExpressions = aggregateDefinitions.map(
|
|
5783
|
+
(aggregate, index) => {
|
|
5784
|
+
const fn = String(aggregate.fn ?? "").toLowerCase();
|
|
5785
|
+
if (!isSupportedAggregateFunction(fn)) {
|
|
5786
|
+
throw new Error(`Unsupported aggregate function '${aggregate.fn}'`);
|
|
5787
|
+
}
|
|
5788
|
+
const hasField = typeof aggregate.field === "string" && aggregate.field.trim().length > 0;
|
|
5789
|
+
if (fn !== "count" && !hasField) {
|
|
5790
|
+
throw new Error(`Aggregate '${fn}' requires a field`);
|
|
5791
|
+
}
|
|
5792
|
+
const fieldExpression = hasField ? snakeCase(String(aggregate.field)) : "*";
|
|
5793
|
+
const distinctPrefix = aggregate.distinct ? "DISTINCT " : "";
|
|
5794
|
+
const expression = fn === "count" && !hasField ? "COUNT(*)" : `${fn.toUpperCase()}(${distinctPrefix}${fieldExpression})`;
|
|
5795
|
+
const alias = buildAggregateAlias(aggregate, index);
|
|
5796
|
+
return `${expression} AS ${alias}`;
|
|
5797
|
+
}
|
|
5798
|
+
);
|
|
5799
|
+
const groupByExpressions = groupByFields.map((field) => snakeCase(field));
|
|
5800
|
+
const selectExpressions = [...groupByExpressions, ...aggregateExpressions];
|
|
5801
|
+
sql = `SELECT ${selectExpressions.join(", ")} FROM ${tableName} ${joinClause} ${whereClause}`;
|
|
5802
|
+
if (groupByExpressions.length > 0) {
|
|
5803
|
+
sql += ` GROUP BY ${groupByExpressions.join(", ")}`;
|
|
5804
|
+
}
|
|
5805
|
+
} else {
|
|
5806
|
+
sql = `SELECT ${fields.length ? fields.map(snakeCase).join(", ") : "*"} FROM ${tableName} ${joinClause} ${whereClause}`;
|
|
5907
5807
|
}
|
|
5908
|
-
if (Object.keys(sort).length > 0) {
|
|
5909
|
-
sql += " ORDER BY " + Object.entries(sort).map(([field, direction]) => `${field} ${direction}`).join(", ");
|
|
5808
|
+
if (Object.keys(sort).length > 0 && resolvedMode !== "count" && resolvedMode !== "exists") {
|
|
5809
|
+
sql += " ORDER BY " + Object.entries(sort).map(([field, direction]) => `${snakeCase(field)} ${direction}`).join(", ");
|
|
5910
5810
|
}
|
|
5911
|
-
if (
|
|
5811
|
+
if (resolvedMode === "one") {
|
|
5812
|
+
sql += ` LIMIT $${params.length + 1}`;
|
|
5813
|
+
params.push(1);
|
|
5814
|
+
} else if (resolvedMode !== "count" && resolvedMode !== "exists" && limit !== void 0) {
|
|
5912
5815
|
sql += ` LIMIT $${params.length + 1}`;
|
|
5913
5816
|
params.push(limit);
|
|
5914
5817
|
}
|
|
5915
|
-
if (offset !== void 0) {
|
|
5818
|
+
if (resolvedMode !== "count" && resolvedMode !== "exists" && offset !== void 0) {
|
|
5916
5819
|
sql += ` OFFSET $${params.length + 1}`;
|
|
5917
5820
|
params.push(offset);
|
|
5918
5821
|
}
|
|
5919
5822
|
try {
|
|
5920
|
-
const result = await this.
|
|
5823
|
+
const result = await this.withTimeout(
|
|
5824
|
+
() => pool.query(sql, params),
|
|
5825
|
+
statementTimeoutMs,
|
|
5826
|
+
`Query timeout on table ${tableName}`
|
|
5827
|
+
);
|
|
5921
5828
|
const rows = this.toCamelCase(result.rows);
|
|
5829
|
+
const rowCount = Number(result.rowCount ?? 0);
|
|
5830
|
+
if (resolvedMode === "count") {
|
|
5831
|
+
return {
|
|
5832
|
+
count: Number(rows[0]?.count ?? 0),
|
|
5833
|
+
rowCount: Number(rows[0]?.count ?? 0),
|
|
5834
|
+
__success: true
|
|
5835
|
+
};
|
|
5836
|
+
}
|
|
5837
|
+
if (resolvedMode === "exists") {
|
|
5838
|
+
const exists = Boolean(rows[0]?.exists);
|
|
5839
|
+
return {
|
|
5840
|
+
exists,
|
|
5841
|
+
rowCount: exists ? 1 : 0,
|
|
5842
|
+
__success: true
|
|
5843
|
+
};
|
|
5844
|
+
}
|
|
5845
|
+
if (resolvedMode === "one") {
|
|
5846
|
+
return {
|
|
5847
|
+
[`${camelCase(tableName)}`]: rows[0] ?? null,
|
|
5848
|
+
rowCount,
|
|
5849
|
+
__success: true
|
|
5850
|
+
};
|
|
5851
|
+
}
|
|
5852
|
+
if (resolvedMode === "aggregate") {
|
|
5853
|
+
return {
|
|
5854
|
+
aggregates: rows,
|
|
5855
|
+
rowCount,
|
|
5856
|
+
__success: true
|
|
5857
|
+
};
|
|
5858
|
+
}
|
|
5922
5859
|
return {
|
|
5923
5860
|
[`${camelCase(tableName)}s`]: rows,
|
|
5924
|
-
rowCount
|
|
5925
|
-
__success: true
|
|
5926
|
-
...context
|
|
5861
|
+
rowCount,
|
|
5862
|
+
__success: true
|
|
5927
5863
|
};
|
|
5928
5864
|
} catch (error) {
|
|
5929
5865
|
return {
|
|
5930
|
-
|
|
5866
|
+
rowCount: 0,
|
|
5931
5867
|
errored: true,
|
|
5932
|
-
__error: `Query failed: ${error
|
|
5868
|
+
__error: `Query failed: ${errorMessage(error)}`,
|
|
5933
5869
|
__success: false
|
|
5934
5870
|
};
|
|
5935
5871
|
}
|
|
5936
5872
|
}
|
|
5937
|
-
|
|
5938
|
-
* Inserts data into the specified database table with optional conflict handling.
|
|
5939
|
-
*
|
|
5940
|
-
* @param {string} tableName - The name of the target database table.
|
|
5941
|
-
* @param {DbOperationPayload} context - The context containing data to insert, transaction settings, field mappings, conflict resolution options, and other configurations.
|
|
5942
|
-
* - `data` (object | array): The data to be inserted into the database.
|
|
5943
|
-
* - `transaction` (boolean): Specifies whether the operation should use a transaction. Defaults to true.
|
|
5944
|
-
* - `fields` (array): The fields to return in the result after insertion.
|
|
5945
|
-
* - `onConflict` (object): Options for handling conflicts on insert.
|
|
5946
|
-
* - `target` (array): Columns to determine conflicts.
|
|
5947
|
-
* - `action` (object): Specifies the action to take on conflict, such as updating specified fields.
|
|
5948
|
-
* - `awaitExists` (object): Specifies foreign key references to wait for to ensure existence before insertion.
|
|
5949
|
-
*
|
|
5950
|
-
* @return {Promise<any>} A promise resolving to the result of the database insert operation, including the inserted rows, the row count, and metadata indicating success or error.
|
|
5951
|
-
*/
|
|
5952
|
-
async insertFunction(tableName, context) {
|
|
5873
|
+
async insertFunction(registration, tableName, context) {
|
|
5953
5874
|
const { data, transaction = true, fields = [], onConflict } = context;
|
|
5954
5875
|
if (!data || Array.isArray(data) && data.length === 0) {
|
|
5955
|
-
return {
|
|
5876
|
+
return {
|
|
5877
|
+
rowCount: 0,
|
|
5878
|
+
errored: true,
|
|
5879
|
+
__error: "No data provided for insert",
|
|
5880
|
+
__success: false
|
|
5881
|
+
};
|
|
5956
5882
|
}
|
|
5957
|
-
|
|
5958
|
-
const
|
|
5883
|
+
const pool = this.getPoolOrThrow(registration);
|
|
5884
|
+
const safetyPolicy = this.resolveSafetyPolicy(registration);
|
|
5959
5885
|
try {
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
}
|
|
5971
|
-
if (value.__effect === "decrement") {
|
|
5972
|
-
return `${Object.keys(row)[i]} - 1`;
|
|
5973
|
-
}
|
|
5974
|
-
if (value.__effect === "set") {
|
|
5975
|
-
return `${Object.keys(row)[i]} = ${value.__value}`;
|
|
5976
|
-
}
|
|
5886
|
+
const resultContext = await this.runWithRetries(
|
|
5887
|
+
async () => this.executeWithTransaction(pool, Boolean(transaction), async (client) => {
|
|
5888
|
+
const resolvedData = await this.resolveNestedData(
|
|
5889
|
+
registration,
|
|
5890
|
+
data,
|
|
5891
|
+
tableName
|
|
5892
|
+
);
|
|
5893
|
+
const rows = Array.isArray(resolvedData) ? resolvedData : [resolvedData];
|
|
5894
|
+
if (rows.length === 0) {
|
|
5895
|
+
throw new Error("No rows available for insert after resolving data");
|
|
5977
5896
|
}
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5897
|
+
const keys = Object.keys(rows[0]);
|
|
5898
|
+
const sqlPrefix = `INSERT INTO ${tableName} (${keys.map((key) => snakeCase(key)).join(", ")}) VALUES `;
|
|
5899
|
+
const params = [];
|
|
5900
|
+
const placeholders = rows.map((row) => {
|
|
5901
|
+
const tuple = keys.map((key) => {
|
|
5902
|
+
params.push(row[key]);
|
|
5903
|
+
return `$${params.length}`;
|
|
5904
|
+
}).join(", ");
|
|
5905
|
+
return `(${tuple})`;
|
|
5906
|
+
}).join(", ");
|
|
5907
|
+
let onConflictSql = "";
|
|
5908
|
+
if (onConflict) {
|
|
5909
|
+
onConflictSql = this.buildOnConflictClause(onConflict, params);
|
|
5989
5910
|
}
|
|
5990
|
-
const
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
(v) => typeof v !== "string" || !v.startsWith("excluded.")
|
|
5996
|
-
)
|
|
5911
|
+
const sql = `${sqlPrefix}${placeholders}${onConflictSql} RETURNING ${fields.length ? fields.map(snakeCase).join(", ") : "*"}`;
|
|
5912
|
+
const result = await this.withTimeout(
|
|
5913
|
+
() => client.query(sql, params),
|
|
5914
|
+
safetyPolicy.statementTimeoutMs,
|
|
5915
|
+
`Insert timeout on table ${tableName}`
|
|
5997
5916
|
);
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
5917
|
+
const resultRows = this.toCamelCase(result.rows);
|
|
5918
|
+
return {
|
|
5919
|
+
[`${camelCase(tableName)}${rows.length > 1 ? "s" : ""}`]: rows.length > 1 ? resultRows : resultRows[0] ?? null,
|
|
5920
|
+
rowCount: result.rowCount,
|
|
5921
|
+
__success: true
|
|
5922
|
+
};
|
|
5923
|
+
}),
|
|
5924
|
+
safetyPolicy,
|
|
5925
|
+
`Insert ${tableName}`
|
|
6007
5926
|
);
|
|
6008
|
-
|
|
6009
|
-
const resultRows = this.toCamelCase(result.rows);
|
|
6010
|
-
resultContext = {
|
|
6011
|
-
[`${camelCase(tableName)}${isBatch ? "s" : ""}`]: isBatch ? resultRows : resultRows[0],
|
|
6012
|
-
rowCount: result.rowCount,
|
|
6013
|
-
__success: true
|
|
6014
|
-
};
|
|
5927
|
+
return resultContext;
|
|
6015
5928
|
} catch (error) {
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
} else {
|
|
6023
|
-
resultContext = {
|
|
6024
|
-
...context,
|
|
6025
|
-
errored: true,
|
|
6026
|
-
__error: `Insert failed: ${error.message}`,
|
|
6027
|
-
__success: false
|
|
6028
|
-
};
|
|
6029
|
-
}
|
|
6030
|
-
} finally {
|
|
6031
|
-
if (transaction && client) {
|
|
6032
|
-
client.release();
|
|
6033
|
-
}
|
|
5929
|
+
return {
|
|
5930
|
+
rowCount: 0,
|
|
5931
|
+
errored: true,
|
|
5932
|
+
__error: `Insert failed: ${errorMessage(error)}`,
|
|
5933
|
+
__success: false
|
|
5934
|
+
};
|
|
6034
5935
|
}
|
|
6035
|
-
return resultContext;
|
|
6036
5936
|
}
|
|
6037
|
-
|
|
6038
|
-
* Updates a database table with the provided data and filter conditions.
|
|
6039
|
-
*
|
|
6040
|
-
* @param {string} tableName - The name of the database table to update.
|
|
6041
|
-
* @param {DbOperationPayload} context - The payload for the update operation, which includes:
|
|
6042
|
-
* - data: The data to update in the table.
|
|
6043
|
-
* - filter: The conditions to identify the rows to update (default is an empty object).
|
|
6044
|
-
* - transaction: Whether the operation should run within a database transaction (default is true).
|
|
6045
|
-
* @return {Promise<any>} Returns a Promise resolving to an object that includes:
|
|
6046
|
-
* - The updated data if the update is successful.
|
|
6047
|
-
* - In case of error:
|
|
6048
|
-
* - Error details.
|
|
6049
|
-
* - The SQL query and parameters if applicable.
|
|
6050
|
-
* - A flag indicating if the update succeeded or failed.
|
|
6051
|
-
*/
|
|
6052
|
-
async updateFunction(tableName, context) {
|
|
5937
|
+
async updateFunction(registration, tableName, context) {
|
|
6053
5938
|
const { data, filter = {}, transaction = true } = context;
|
|
6054
5939
|
if (!data || Object.keys(data).length === 0) {
|
|
6055
5940
|
return {
|
|
5941
|
+
rowCount: 0,
|
|
6056
5942
|
errored: true,
|
|
6057
|
-
__error: `No data provided for update of ${tableName}
|
|
5943
|
+
__error: `No data provided for update of ${tableName}`,
|
|
5944
|
+
__success: false
|
|
6058
5945
|
};
|
|
6059
5946
|
}
|
|
6060
|
-
|
|
6061
|
-
const
|
|
5947
|
+
const pool = this.getPoolOrThrow(registration);
|
|
5948
|
+
const safetyPolicy = this.resolveSafetyPolicy(registration);
|
|
6062
5949
|
try {
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
5950
|
+
return await this.runWithRetries(
|
|
5951
|
+
async () => this.executeWithTransaction(pool, Boolean(transaction), async (client) => {
|
|
5952
|
+
const resolvedData = await this.resolveNestedData(
|
|
5953
|
+
registration,
|
|
5954
|
+
data,
|
|
5955
|
+
tableName
|
|
5956
|
+
);
|
|
5957
|
+
const params = Object.values(resolvedData);
|
|
5958
|
+
let offset = 0;
|
|
5959
|
+
const setClause = Object.keys(resolvedData).map((key, index) => {
|
|
5960
|
+
const value = resolvedData[key];
|
|
5961
|
+
const offsetIndex = index - offset;
|
|
5962
|
+
if (value?.__effect === "increment") {
|
|
5963
|
+
params.splice(offsetIndex, 1);
|
|
5964
|
+
offset += 1;
|
|
5965
|
+
return `${snakeCase(key)} = ${snakeCase(key)} + 1`;
|
|
5966
|
+
}
|
|
5967
|
+
if (value?.__effect === "decrement") {
|
|
5968
|
+
params.splice(offsetIndex, 1);
|
|
5969
|
+
offset += 1;
|
|
5970
|
+
return `${snakeCase(key)} = ${snakeCase(key)} - 1`;
|
|
5971
|
+
}
|
|
5972
|
+
if (value?.__effect === "set") {
|
|
5973
|
+
params.splice(offsetIndex, 1);
|
|
5974
|
+
offset += 1;
|
|
5975
|
+
return `${snakeCase(key)} = ${value.__value}`;
|
|
5976
|
+
}
|
|
5977
|
+
return `${snakeCase(key)} = $${offsetIndex + 1}`;
|
|
5978
|
+
}).join(", ");
|
|
5979
|
+
const whereClause = this.buildWhereClause(filter, params);
|
|
5980
|
+
const sql = `UPDATE ${tableName} SET ${setClause} ${whereClause} RETURNING *`;
|
|
5981
|
+
const result = await this.withTimeout(
|
|
5982
|
+
() => client.query(sql, params),
|
|
5983
|
+
safetyPolicy.statementTimeoutMs,
|
|
5984
|
+
`Update timeout on table ${tableName}`
|
|
5985
|
+
);
|
|
5986
|
+
const rows = this.toCamelCase(result.rows);
|
|
5987
|
+
const rowCount = Number(result.rowCount ?? 0);
|
|
5988
|
+
return {
|
|
5989
|
+
[`${camelCase(tableName)}`]: rows[0] ?? null,
|
|
5990
|
+
rowCount,
|
|
5991
|
+
__success: rowCount > 0
|
|
5992
|
+
};
|
|
5993
|
+
}),
|
|
5994
|
+
safetyPolicy,
|
|
5995
|
+
`Update ${tableName}`
|
|
5996
|
+
);
|
|
6104
5997
|
} catch (error) {
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
...context,
|
|
5998
|
+
return {
|
|
5999
|
+
rowCount: 0,
|
|
6108
6000
|
errored: true,
|
|
6109
|
-
__error: `Update failed: ${error
|
|
6001
|
+
__error: `Update failed: ${errorMessage(error)}`,
|
|
6110
6002
|
__success: false
|
|
6111
6003
|
};
|
|
6112
|
-
} finally {
|
|
6113
|
-
if (transaction && client) {
|
|
6114
|
-
client.release();
|
|
6115
|
-
}
|
|
6116
6004
|
}
|
|
6117
|
-
return resultContext;
|
|
6118
6005
|
}
|
|
6119
|
-
|
|
6120
|
-
* Deletes a record from the specified database table based on the given filter criteria.
|
|
6121
|
-
*
|
|
6122
|
-
* @param {string} tableName - The name of the database table from which records should be deleted.
|
|
6123
|
-
* @param {DbOperationPayload} context - The context for the operation, including filter conditions and transaction settings.
|
|
6124
|
-
* @param {Object} context.filter - The filter criteria to identify the records to delete.
|
|
6125
|
-
* @param {boolean} [context.transaction=true] - Indicates if the operation should be executed within a transaction.
|
|
6126
|
-
* @return {Promise<any>} A promise that resolves to an object containing information about the deleted record
|
|
6127
|
-
* or an error object if the delete operation fails.
|
|
6128
|
-
*/
|
|
6129
|
-
async deleteFunction(tableName, context) {
|
|
6006
|
+
async deleteFunction(registration, tableName, context) {
|
|
6130
6007
|
const { filter = {}, transaction = true } = context;
|
|
6131
6008
|
if (Object.keys(filter).length === 0) {
|
|
6132
|
-
return {
|
|
6009
|
+
return {
|
|
6010
|
+
rowCount: 0,
|
|
6011
|
+
errored: true,
|
|
6012
|
+
__error: "No filter provided for delete",
|
|
6013
|
+
__success: false
|
|
6014
|
+
};
|
|
6133
6015
|
}
|
|
6134
|
-
|
|
6135
|
-
const
|
|
6016
|
+
const pool = this.getPoolOrThrow(registration);
|
|
6017
|
+
const safetyPolicy = this.resolveSafetyPolicy(registration);
|
|
6136
6018
|
try {
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6019
|
+
return await this.runWithRetries(
|
|
6020
|
+
async () => this.executeWithTransaction(pool, Boolean(transaction), async (client) => {
|
|
6021
|
+
const params = [];
|
|
6022
|
+
const whereClause = this.buildWhereClause(filter, params);
|
|
6023
|
+
const sql = `DELETE FROM ${tableName} ${whereClause} RETURNING *`;
|
|
6024
|
+
const result = await this.withTimeout(
|
|
6025
|
+
() => client.query(sql, params),
|
|
6026
|
+
safetyPolicy.statementTimeoutMs,
|
|
6027
|
+
`Delete timeout on table ${tableName}`
|
|
6028
|
+
);
|
|
6029
|
+
const rows = this.toCamelCase(result.rows);
|
|
6030
|
+
return {
|
|
6031
|
+
[`${camelCase(tableName)}`]: rows[0] ?? null,
|
|
6032
|
+
rowCount: result.rowCount,
|
|
6033
|
+
__success: true
|
|
6034
|
+
};
|
|
6035
|
+
}),
|
|
6036
|
+
safetyPolicy,
|
|
6037
|
+
`Delete ${tableName}`
|
|
6038
|
+
);
|
|
6148
6039
|
} catch (error) {
|
|
6149
|
-
|
|
6150
|
-
|
|
6040
|
+
return {
|
|
6041
|
+
rowCount: 0,
|
|
6151
6042
|
errored: true,
|
|
6152
|
-
__error: `Delete failed: ${error
|
|
6153
|
-
__errors: { delete: error.message },
|
|
6043
|
+
__error: `Delete failed: ${errorMessage(error)}`,
|
|
6154
6044
|
__success: false
|
|
6155
6045
|
};
|
|
6156
|
-
}
|
|
6157
|
-
|
|
6158
|
-
|
|
6046
|
+
}
|
|
6047
|
+
}
|
|
6048
|
+
resolveSafetyPolicy(registration) {
|
|
6049
|
+
const durableState = registration.actor.getState(
|
|
6050
|
+
registration.actorKey
|
|
6051
|
+
);
|
|
6052
|
+
return {
|
|
6053
|
+
statementTimeoutMs: normalizePositiveInteger(
|
|
6054
|
+
durableState.safetyPolicy?.statementTimeoutMs,
|
|
6055
|
+
15e3
|
|
6056
|
+
),
|
|
6057
|
+
retryCount: normalizePositiveInteger(durableState.safetyPolicy?.retryCount, 3, 0),
|
|
6058
|
+
retryDelayMs: normalizePositiveInteger(durableState.safetyPolicy?.retryDelayMs, 100),
|
|
6059
|
+
retryDelayMaxMs: normalizePositiveInteger(
|
|
6060
|
+
durableState.safetyPolicy?.retryDelayMaxMs,
|
|
6061
|
+
1e3
|
|
6062
|
+
),
|
|
6063
|
+
retryDelayFactor: Number.isFinite(durableState.safetyPolicy?.retryDelayFactor) ? Math.max(1, Number(durableState.safetyPolicy?.retryDelayFactor)) : 1
|
|
6064
|
+
};
|
|
6065
|
+
}
|
|
6066
|
+
buildOnConflictClause(onConflict, params) {
|
|
6067
|
+
const { target, action } = onConflict;
|
|
6068
|
+
let sql = ` ON CONFLICT (${target.map(snakeCase).join(", ")})`;
|
|
6069
|
+
if (action.do === "update") {
|
|
6070
|
+
if (!action.set || Object.keys(action.set).length === 0) {
|
|
6071
|
+
throw new Error("Update action requires 'set' fields");
|
|
6072
|
+
}
|
|
6073
|
+
const assignments = Object.entries(action.set).map(([field, value]) => {
|
|
6074
|
+
if (typeof value === "string" && value === "excluded") {
|
|
6075
|
+
return `${snakeCase(field)} = excluded.${snakeCase(field)}`;
|
|
6076
|
+
}
|
|
6077
|
+
params.push(value);
|
|
6078
|
+
return `${snakeCase(field)} = $${params.length}`;
|
|
6079
|
+
});
|
|
6080
|
+
sql += ` DO UPDATE SET ${assignments.join(", ")}`;
|
|
6081
|
+
if (action.where) {
|
|
6082
|
+
sql += ` WHERE ${action.where}`;
|
|
6159
6083
|
}
|
|
6084
|
+
return sql;
|
|
6160
6085
|
}
|
|
6161
|
-
|
|
6086
|
+
sql += " DO NOTHING";
|
|
6087
|
+
return sql;
|
|
6162
6088
|
}
|
|
6163
6089
|
/**
|
|
6164
|
-
*
|
|
6165
|
-
* Builds parameterized queries to prevent SQL injection, appending parameters
|
|
6166
|
-
* to the provided params array and utilizing placeholders.
|
|
6167
|
-
*
|
|
6168
|
-
* @param {Object} filter - An object representing the filtering conditions with
|
|
6169
|
-
* keys as column names and values as their corresponding
|
|
6170
|
-
* desired values. Values can also be arrays for `IN` queries.
|
|
6171
|
-
* @param {any[]} params - An array for storing parameterized values, which will be
|
|
6172
|
-
* populated with the filter values for the constructed SQL clause.
|
|
6173
|
-
* @return {string} The constructed SQL WHERE clause as a string. If no conditions
|
|
6174
|
-
* are provided, an empty string is returned.
|
|
6090
|
+
* Validates database schema structure and content.
|
|
6175
6091
|
*/
|
|
6176
|
-
|
|
6177
|
-
const
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6092
|
+
validateSchema(ctx) {
|
|
6093
|
+
const schema = ctx.schema;
|
|
6094
|
+
if (!schema?.tables || typeof schema.tables !== "object") {
|
|
6095
|
+
throw new Error("Invalid schema: missing or invalid tables");
|
|
6096
|
+
}
|
|
6097
|
+
for (const [tableName, table] of Object.entries(schema.tables)) {
|
|
6098
|
+
if (!isSqlIdentifier(tableName)) {
|
|
6099
|
+
throw new Error(
|
|
6100
|
+
`Invalid table name ${tableName}. Table names must use lowercase snake_case identifiers.`
|
|
6101
|
+
);
|
|
6102
|
+
}
|
|
6103
|
+
if (!table.fields || typeof table.fields !== "object") {
|
|
6104
|
+
throw new Error(`Invalid table ${tableName}: missing fields`);
|
|
6105
|
+
}
|
|
6106
|
+
for (const [fieldName, field] of Object.entries(table.fields)) {
|
|
6107
|
+
if (!isSqlIdentifier(fieldName)) {
|
|
6108
|
+
throw new Error(
|
|
6109
|
+
`Invalid field name ${fieldName} for ${tableName}. Field names must use lowercase snake_case identifiers.`
|
|
6110
|
+
);
|
|
6111
|
+
}
|
|
6112
|
+
if (!SCHEMA_TYPES.includes(field.type)) {
|
|
6113
|
+
throw new Error(`Invalid type ${field.type} for ${tableName}.${fieldName}`);
|
|
6114
|
+
}
|
|
6115
|
+
if (field.references && !field.references.match(/^[\w]+\([\w]+\)$/)) {
|
|
6116
|
+
throw new Error(
|
|
6117
|
+
`Invalid reference ${field.references} for ${tableName}.${fieldName}`
|
|
6187
6118
|
);
|
|
6188
|
-
}
|
|
6189
|
-
|
|
6190
|
-
|
|
6119
|
+
}
|
|
6120
|
+
}
|
|
6121
|
+
for (const operation of ["query", "insert", "update", "delete"]) {
|
|
6122
|
+
const customIntents = table.customIntents?.[operation] ?? [];
|
|
6123
|
+
if (!Array.isArray(customIntents)) {
|
|
6124
|
+
throw new Error(
|
|
6125
|
+
`Invalid customIntents.${operation} for table ${tableName}: expected array`
|
|
6126
|
+
);
|
|
6127
|
+
}
|
|
6128
|
+
for (const customIntent of customIntents) {
|
|
6129
|
+
const parsed = readCustomIntentConfig(
|
|
6130
|
+
customIntent
|
|
6131
|
+
);
|
|
6132
|
+
validateIntentName(String(parsed.intent ?? ""));
|
|
6191
6133
|
}
|
|
6192
6134
|
}
|
|
6193
6135
|
}
|
|
6194
|
-
return
|
|
6136
|
+
return true;
|
|
6195
6137
|
}
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6138
|
+
sortTablesByReferences(ctx) {
|
|
6139
|
+
const schema = ctx.schema;
|
|
6140
|
+
const graph = /* @__PURE__ */ new Map();
|
|
6141
|
+
const allTables = Object.keys(schema.tables);
|
|
6142
|
+
allTables.forEach((table) => graph.set(table, /* @__PURE__ */ new Set()));
|
|
6143
|
+
for (const [tableName, table] of Object.entries(schema.tables)) {
|
|
6144
|
+
for (const field of Object.values(table.fields)) {
|
|
6145
|
+
if (field.references) {
|
|
6146
|
+
const [refTable] = field.references.split("(");
|
|
6147
|
+
if (refTable !== tableName && allTables.includes(refTable)) {
|
|
6148
|
+
graph.get(refTable)?.add(tableName);
|
|
6149
|
+
}
|
|
6150
|
+
}
|
|
6151
|
+
}
|
|
6152
|
+
if (table.foreignKeys) {
|
|
6153
|
+
for (const foreignKey of table.foreignKeys) {
|
|
6154
|
+
const refTable = foreignKey.tableName;
|
|
6155
|
+
if (refTable !== tableName && allTables.includes(refTable)) {
|
|
6156
|
+
graph.get(refTable)?.add(tableName);
|
|
6157
|
+
}
|
|
6158
|
+
}
|
|
6159
|
+
}
|
|
6208
6160
|
}
|
|
6209
|
-
|
|
6161
|
+
const visited = /* @__PURE__ */ new Set();
|
|
6162
|
+
const tempMark = /* @__PURE__ */ new Set();
|
|
6163
|
+
const sorted = [];
|
|
6164
|
+
let hasCycles = false;
|
|
6165
|
+
const visit = (table) => {
|
|
6166
|
+
if (tempMark.has(table)) {
|
|
6167
|
+
hasCycles = true;
|
|
6168
|
+
return;
|
|
6169
|
+
}
|
|
6170
|
+
if (visited.has(table)) return;
|
|
6171
|
+
tempMark.add(table);
|
|
6172
|
+
for (const dependent of graph.get(table) || []) {
|
|
6173
|
+
visit(dependent);
|
|
6174
|
+
}
|
|
6175
|
+
tempMark.delete(table);
|
|
6176
|
+
visited.add(table);
|
|
6177
|
+
sorted.push(table);
|
|
6178
|
+
};
|
|
6179
|
+
for (const table of allTables) {
|
|
6180
|
+
if (!visited.has(table)) {
|
|
6181
|
+
visit(table);
|
|
6182
|
+
}
|
|
6183
|
+
}
|
|
6184
|
+
for (const table of allTables) {
|
|
6185
|
+
if (!visited.has(table)) {
|
|
6186
|
+
sorted.push(table);
|
|
6187
|
+
}
|
|
6188
|
+
}
|
|
6189
|
+
sorted.reverse();
|
|
6190
|
+
return { ...ctx, sortedTables: sorted, hasCycles };
|
|
6210
6191
|
}
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6192
|
+
buildSchemaDdlStatements(schema, sortedTables) {
|
|
6193
|
+
const ddl = [];
|
|
6194
|
+
for (const tableName of sortedTables) {
|
|
6195
|
+
const table = schema.tables[tableName];
|
|
6196
|
+
const fieldDefs = Object.entries(table.fields).map(([fieldName, field]) => this.fieldDefinitionToSql(fieldName, field)).join(", ");
|
|
6197
|
+
ddl.push(`CREATE TABLE IF NOT EXISTS ${tableName} (${fieldDefs});`);
|
|
6198
|
+
for (const indexFields of table.indexes ?? []) {
|
|
6199
|
+
ddl.push(
|
|
6200
|
+
`CREATE INDEX IF NOT EXISTS idx_${tableName}_${indexFields.join("_")} ON ${tableName} (${indexFields.map(snakeCase).join(", ")});`
|
|
6201
|
+
);
|
|
6202
|
+
}
|
|
6203
|
+
if (table.primaryKey) {
|
|
6204
|
+
ddl.push(
|
|
6205
|
+
`ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS pk_${tableName}_${table.primaryKey.join("_")};`,
|
|
6206
|
+
`ALTER TABLE ${tableName} ADD CONSTRAINT pk_${tableName}_${table.primaryKey.join("_")} PRIMARY KEY (${table.primaryKey.map(snakeCase).join(", ")});`
|
|
6207
|
+
);
|
|
6208
|
+
}
|
|
6209
|
+
for (const uniqueFields of table.uniqueConstraints ?? []) {
|
|
6210
|
+
ddl.push(
|
|
6211
|
+
`ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS uq_${tableName}_${uniqueFields.join("_")};`,
|
|
6212
|
+
`ALTER TABLE ${tableName} ADD CONSTRAINT uq_${tableName}_${uniqueFields.join("_")} UNIQUE (${uniqueFields.map(snakeCase).join(", ")});`
|
|
6213
|
+
);
|
|
6214
|
+
}
|
|
6215
|
+
for (const foreignKey of table.foreignKeys ?? []) {
|
|
6216
|
+
const fkName = `fk_${tableName}_${foreignKey.fields.join("_")}`;
|
|
6217
|
+
ddl.push(
|
|
6218
|
+
`ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS ${fkName};`,
|
|
6219
|
+
`ALTER TABLE ${tableName} ADD CONSTRAINT ${fkName} FOREIGN KEY (${foreignKey.fields.map(snakeCase).join(", ")}) REFERENCES ${foreignKey.tableName} (${foreignKey.referenceFields.map(snakeCase).join(", ")});`
|
|
6220
|
+
);
|
|
6221
|
+
}
|
|
6222
|
+
for (const [triggerName, trigger] of Object.entries(table.triggers ?? {})) {
|
|
6223
|
+
ddl.push(
|
|
6224
|
+
`CREATE OR REPLACE TRIGGER ${triggerName} ${trigger.when} ${trigger.event} ON ${tableName} FOR EACH STATEMENT EXECUTE FUNCTION ${trigger.function};`
|
|
6225
|
+
);
|
|
6226
|
+
}
|
|
6227
|
+
if (table.initialData) {
|
|
6228
|
+
ddl.push(
|
|
6229
|
+
`INSERT INTO ${tableName} (${table.initialData.fields.map(snakeCase).join(", ")}) VALUES ${table.initialData.data.map(
|
|
6230
|
+
(row) => `(${row.map((value) => {
|
|
6231
|
+
if (value === void 0) return "NULL";
|
|
6232
|
+
if (value === null) return "NULL";
|
|
6233
|
+
if (typeof value === "number") return String(value);
|
|
6234
|
+
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
|
|
6235
|
+
const stringValue = String(value);
|
|
6236
|
+
return `'${stringValue.replace(/'/g, "''")}'`;
|
|
6237
|
+
}).join(", ")})`
|
|
6238
|
+
).join(", ")} ON CONFLICT DO NOTHING;`
|
|
6239
|
+
);
|
|
6232
6240
|
}
|
|
6233
6241
|
}
|
|
6234
|
-
return
|
|
6242
|
+
return ddl;
|
|
6235
6243
|
}
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6244
|
+
fieldDefinitionToSql(fieldName, field) {
|
|
6245
|
+
let definition = `${snakeCase(fieldName)} ${field.type.toUpperCase()}`;
|
|
6246
|
+
if (field.type === "varchar") {
|
|
6247
|
+
definition += `(${field.constraints?.maxLength ?? 255})`;
|
|
6248
|
+
}
|
|
6249
|
+
if (field.type === "decimal") {
|
|
6250
|
+
definition += `(${field.constraints?.precision ?? 10},${field.constraints?.scale ?? 2})`;
|
|
6251
|
+
}
|
|
6252
|
+
if (field.primary) definition += " PRIMARY KEY";
|
|
6253
|
+
if (field.unique) definition += " UNIQUE";
|
|
6254
|
+
if (field.default !== void 0) {
|
|
6255
|
+
definition += ` DEFAULT ${field.default === "" ? "''" : String(field.default)}`;
|
|
6256
|
+
}
|
|
6257
|
+
if (field.required && !field.nullable) definition += " NOT NULL";
|
|
6258
|
+
if (field.nullable) definition += " NULL";
|
|
6259
|
+
if (field.generated) {
|
|
6260
|
+
definition += ` GENERATED ALWAYS AS ${field.generated.toUpperCase()} STORED`;
|
|
6261
|
+
}
|
|
6262
|
+
if (field.references) {
|
|
6263
|
+
definition += ` REFERENCES ${field.references} ON DELETE ${field.onDelete || "CASCADE"}`;
|
|
6264
|
+
}
|
|
6265
|
+
if (field.constraints?.check) {
|
|
6266
|
+
definition += ` CHECK (${field.constraints.check})`;
|
|
6267
|
+
}
|
|
6268
|
+
return definition;
|
|
6269
|
+
}
|
|
6270
|
+
async applyDdlStatements(pool, statements) {
|
|
6271
|
+
for (const sql of statements) {
|
|
6272
|
+
try {
|
|
6273
|
+
await pool.query(sql);
|
|
6274
|
+
} catch (error) {
|
|
6275
|
+
CadenzaService.log(
|
|
6276
|
+
"Error applying DDL statement",
|
|
6277
|
+
{
|
|
6278
|
+
sql,
|
|
6279
|
+
error: errorMessage(error)
|
|
6280
|
+
},
|
|
6281
|
+
"error"
|
|
6282
|
+
);
|
|
6283
|
+
throw error;
|
|
6265
6284
|
}
|
|
6266
|
-
await client.query("COMMIT");
|
|
6267
|
-
return result || {};
|
|
6268
|
-
} catch (error) {
|
|
6269
|
-
await client.query("ROLLBACK");
|
|
6270
|
-
throw error;
|
|
6271
|
-
} finally {
|
|
6272
|
-
client.release();
|
|
6273
6285
|
}
|
|
6274
6286
|
}
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
const
|
|
6287
|
-
const
|
|
6287
|
+
generateDatabaseTasks(registration) {
|
|
6288
|
+
for (const [tableName, table] of Object.entries(registration.schema.tables)) {
|
|
6289
|
+
this.createDatabaseTask(registration, "query", tableName, table);
|
|
6290
|
+
this.createDatabaseTask(registration, "insert", tableName, table);
|
|
6291
|
+
this.createDatabaseTask(registration, "update", tableName, table);
|
|
6292
|
+
this.createDatabaseTask(registration, "delete", tableName, table);
|
|
6293
|
+
this.createDatabaseMacroTasks(registration, tableName, table);
|
|
6294
|
+
}
|
|
6295
|
+
}
|
|
6296
|
+
createDatabaseMacroTasks(registration, tableName, table) {
|
|
6297
|
+
const querySchema = this.getInputSchema("query", tableName, table);
|
|
6298
|
+
const insertSchema = this.getInputSchema("insert", tableName, table);
|
|
6299
|
+
const queryMacroOperations = [
|
|
6300
|
+
"count",
|
|
6301
|
+
"exists",
|
|
6302
|
+
"one",
|
|
6303
|
+
"aggregate"
|
|
6304
|
+
];
|
|
6305
|
+
for (const macroOperation of queryMacroOperations) {
|
|
6306
|
+
const intentName = `${macroOperation}-pg-${registration.actorToken}-${tableName}`;
|
|
6307
|
+
if (registration.intentNames.has(intentName)) {
|
|
6308
|
+
throw new Error(
|
|
6309
|
+
`Duplicate macro intent '${intentName}' detected for table '${tableName}' in actor '${registration.actorName}'`
|
|
6310
|
+
);
|
|
6311
|
+
}
|
|
6312
|
+
registration.intentNames.add(intentName);
|
|
6313
|
+
CadenzaService.defineIntent({
|
|
6314
|
+
name: intentName,
|
|
6315
|
+
description: `Macro ${macroOperation} operation for table ${tableName}`,
|
|
6316
|
+
input: querySchema
|
|
6317
|
+
});
|
|
6318
|
+
CadenzaService.createThrottledTask(
|
|
6319
|
+
`${macroOperation.toUpperCase()} ${tableName}`,
|
|
6320
|
+
registration.actor.task(
|
|
6321
|
+
async ({ input }) => {
|
|
6322
|
+
const payload = typeof input.queryData === "object" && input.queryData ? input.queryData : input;
|
|
6323
|
+
const result = await this.queryFunction(registration, tableName, {
|
|
6324
|
+
...payload,
|
|
6325
|
+
queryMode: macroOperation
|
|
6326
|
+
});
|
|
6327
|
+
return {
|
|
6328
|
+
...input,
|
|
6329
|
+
...result
|
|
6330
|
+
};
|
|
6331
|
+
},
|
|
6332
|
+
{ mode: "read" }
|
|
6333
|
+
),
|
|
6334
|
+
(context) => context?.__metadata?.__executionTraceId ?? context?.__executionTraceId ?? "default",
|
|
6335
|
+
`Macro ${macroOperation} task for ${tableName}`,
|
|
6336
|
+
{
|
|
6337
|
+
isMeta: registration.options.isMeta,
|
|
6338
|
+
isSubMeta: registration.options.isMeta,
|
|
6339
|
+
validateInputContext: registration.options.securityProfile !== "low",
|
|
6340
|
+
inputSchema: querySchema
|
|
6341
|
+
}
|
|
6342
|
+
).respondsTo(intentName);
|
|
6343
|
+
}
|
|
6344
|
+
const upsertIntentName = `upsert-pg-${registration.actorToken}-${tableName}`;
|
|
6345
|
+
if (registration.intentNames.has(upsertIntentName)) {
|
|
6346
|
+
throw new Error(
|
|
6347
|
+
`Duplicate macro intent '${upsertIntentName}' detected for table '${tableName}' in actor '${registration.actorName}'`
|
|
6348
|
+
);
|
|
6349
|
+
}
|
|
6350
|
+
registration.intentNames.add(upsertIntentName);
|
|
6351
|
+
CadenzaService.defineIntent({
|
|
6352
|
+
name: upsertIntentName,
|
|
6353
|
+
description: `Macro upsert operation for table ${tableName}`,
|
|
6354
|
+
input: insertSchema
|
|
6355
|
+
});
|
|
6356
|
+
CadenzaService.createThrottledTask(
|
|
6357
|
+
`UPSERT ${tableName}`,
|
|
6358
|
+
registration.actor.task(
|
|
6359
|
+
async ({ input }) => {
|
|
6360
|
+
const payload = typeof input.queryData === "object" && input.queryData ? input.queryData : input;
|
|
6361
|
+
if (!payload.onConflict) {
|
|
6362
|
+
return {
|
|
6363
|
+
...input,
|
|
6364
|
+
errored: true,
|
|
6365
|
+
__success: false,
|
|
6366
|
+
__error: `Macro upsert requires 'onConflict' payload for table '${tableName}'`
|
|
6367
|
+
};
|
|
6368
|
+
}
|
|
6369
|
+
const result = await this.insertFunction(registration, tableName, payload);
|
|
6370
|
+
return {
|
|
6371
|
+
...input,
|
|
6372
|
+
...result
|
|
6373
|
+
};
|
|
6374
|
+
},
|
|
6375
|
+
{ mode: "write" }
|
|
6376
|
+
),
|
|
6377
|
+
(context) => context?.__metadata?.__executionTraceId ?? context?.__executionTraceId ?? "default",
|
|
6378
|
+
`Macro upsert task for ${tableName}`,
|
|
6379
|
+
{
|
|
6380
|
+
isMeta: registration.options.isMeta,
|
|
6381
|
+
isSubMeta: registration.options.isMeta,
|
|
6382
|
+
validateInputContext: registration.options.securityProfile !== "low",
|
|
6383
|
+
inputSchema: insertSchema
|
|
6384
|
+
}
|
|
6385
|
+
).respondsTo(upsertIntentName);
|
|
6386
|
+
}
|
|
6387
|
+
createDatabaseTask(registration, op, tableName, table) {
|
|
6388
|
+
const opAction = op === "query" ? "queried" : op === "insert" ? "inserted" : op === "update" ? "updated" : "deleted";
|
|
6389
|
+
const defaultSignal = `global.${registration.options.isMeta ? "meta." : ""}${tableName}.${opAction}`;
|
|
6288
6390
|
const taskName = `${op.charAt(0).toUpperCase() + op.slice(1)} ${tableName}`;
|
|
6289
6391
|
const schema = this.getInputSchema(op, tableName, table);
|
|
6290
|
-
const
|
|
6291
|
-
|
|
6292
|
-
|
|
6392
|
+
const databaseTaskFunction = registration.actor.task(
|
|
6393
|
+
async ({ input, emit }) => {
|
|
6394
|
+
let context = { ...input };
|
|
6395
|
+
let payloadModifiedByTriggers = false;
|
|
6293
6396
|
for (const action of Object.keys(table.customSignals?.triggers ?? {})) {
|
|
6294
|
-
const
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
for (const triggerCondition of triggerConditions ?? []) {
|
|
6301
|
-
if (triggerCondition.condition && !triggerCondition.condition(context)) {
|
|
6397
|
+
const triggerDefinitions = table.customSignals?.triggers?.[action];
|
|
6398
|
+
for (const trigger of triggerDefinitions ?? []) {
|
|
6399
|
+
if (typeof trigger === "string") {
|
|
6400
|
+
continue;
|
|
6401
|
+
}
|
|
6402
|
+
if (trigger.condition && !trigger.condition(context)) {
|
|
6302
6403
|
return {
|
|
6303
6404
|
failed: true,
|
|
6304
|
-
|
|
6405
|
+
__success: false,
|
|
6406
|
+
__error: `Condition for signal trigger failed: ${trigger.signal}`
|
|
6305
6407
|
};
|
|
6306
6408
|
}
|
|
6307
|
-
|
|
6308
|
-
const triggerQueryData = (
|
|
6309
|
-
// @ts-ignore
|
|
6310
|
-
table.customSignals?.triggers?.[action].filter(
|
|
6311
|
-
(trigger) => trigger.queryData
|
|
6312
|
-
)
|
|
6313
|
-
);
|
|
6314
|
-
for (const queryData of triggerQueryData ?? []) {
|
|
6315
|
-
if (context.queryData) {
|
|
6409
|
+
if (trigger.queryData) {
|
|
6316
6410
|
context.queryData = {
|
|
6317
|
-
...context.queryData,
|
|
6318
|
-
...queryData
|
|
6319
|
-
};
|
|
6320
|
-
} else {
|
|
6321
|
-
context = {
|
|
6322
|
-
...context,
|
|
6323
|
-
...queryData
|
|
6411
|
+
...context.queryData ?? {},
|
|
6412
|
+
...trigger.queryData
|
|
6324
6413
|
};
|
|
6414
|
+
payloadModifiedByTriggers = true;
|
|
6325
6415
|
}
|
|
6326
6416
|
}
|
|
6327
6417
|
}
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6418
|
+
const operationPayload = typeof context.queryData === "object" && context.queryData ? context.queryData : context;
|
|
6419
|
+
this.validateOperationPayload(
|
|
6420
|
+
registration,
|
|
6421
|
+
op,
|
|
6422
|
+
tableName,
|
|
6423
|
+
table,
|
|
6424
|
+
operationPayload,
|
|
6425
|
+
{
|
|
6426
|
+
enforceFieldAllowlist: registration.options.securityProfile === "low" || payloadModifiedByTriggers
|
|
6427
|
+
}
|
|
6428
|
+
);
|
|
6429
|
+
let result;
|
|
6430
|
+
if (op === "query") {
|
|
6431
|
+
result = await this.queryFunction(registration, tableName, operationPayload);
|
|
6432
|
+
} else if (op === "insert") {
|
|
6433
|
+
result = await this.insertFunction(registration, tableName, operationPayload);
|
|
6434
|
+
} else if (op === "update") {
|
|
6435
|
+
result = await this.updateFunction(registration, tableName, operationPayload);
|
|
6436
|
+
} else {
|
|
6437
|
+
result = await this.deleteFunction(registration, tableName, operationPayload);
|
|
6344
6438
|
}
|
|
6439
|
+
context = {
|
|
6440
|
+
...context,
|
|
6441
|
+
...result
|
|
6442
|
+
};
|
|
6345
6443
|
if (!context.errored) {
|
|
6346
6444
|
for (const signal of table.customSignals?.emissions?.[op] ?? []) {
|
|
6445
|
+
if (typeof signal === "string") {
|
|
6446
|
+
emit(signal, context);
|
|
6447
|
+
continue;
|
|
6448
|
+
}
|
|
6347
6449
|
if (signal.condition && !signal.condition(context)) {
|
|
6348
6450
|
continue;
|
|
6349
6451
|
}
|
|
6350
|
-
emit(signal.signal
|
|
6452
|
+
emit(signal.signal, context);
|
|
6351
6453
|
}
|
|
6352
6454
|
}
|
|
6353
6455
|
if (tableName !== "system_log" && context.errored) {
|
|
@@ -6377,49 +6479,263 @@ var DatabaseController = class _DatabaseController {
|
|
|
6377
6479
|
delete context.offset;
|
|
6378
6480
|
return context;
|
|
6379
6481
|
},
|
|
6482
|
+
{ mode: op === "query" ? "read" : "write" }
|
|
6483
|
+
);
|
|
6484
|
+
const task = CadenzaService.createThrottledTask(
|
|
6485
|
+
taskName,
|
|
6486
|
+
databaseTaskFunction,
|
|
6380
6487
|
(context) => context?.__metadata?.__executionTraceId ?? context?.__executionTraceId ?? "default",
|
|
6381
|
-
`Auto-generated ${op} task for ${tableName}`,
|
|
6488
|
+
`Auto-generated ${op} task for ${tableName} (PostgresActor)`,
|
|
6382
6489
|
{
|
|
6383
|
-
isMeta: options.isMeta,
|
|
6384
|
-
isSubMeta: options.isMeta,
|
|
6385
|
-
validateInputContext: options.securityProfile !== "low",
|
|
6490
|
+
isMeta: registration.options.isMeta,
|
|
6491
|
+
isSubMeta: registration.options.isMeta,
|
|
6492
|
+
validateInputContext: registration.options.securityProfile !== "low",
|
|
6386
6493
|
inputSchema: schema
|
|
6387
6494
|
}
|
|
6388
|
-
).doOn(
|
|
6389
|
-
...table.customSignals?.triggers?.[op]?.map(
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
).emits(defaultSignal).attachSignal(
|
|
6393
|
-
...table.customSignals?.emissions?.[op]?.map(
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
);
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6495
|
+
).doOn(
|
|
6496
|
+
...table.customSignals?.triggers?.[op]?.map(
|
|
6497
|
+
(signal) => typeof signal === "string" ? signal : signal.signal
|
|
6498
|
+
) ?? []
|
|
6499
|
+
).emits(defaultSignal).attachSignal(
|
|
6500
|
+
...table.customSignals?.emissions?.[op]?.map(
|
|
6501
|
+
(signal) => typeof signal === "string" ? signal : signal.signal
|
|
6502
|
+
) ?? []
|
|
6503
|
+
);
|
|
6504
|
+
const { intents } = resolveTableOperationIntents(
|
|
6505
|
+
registration.actorName,
|
|
6506
|
+
tableName,
|
|
6507
|
+
table,
|
|
6508
|
+
op,
|
|
6509
|
+
schema
|
|
6510
|
+
);
|
|
6511
|
+
for (const intent of intents) {
|
|
6512
|
+
if (registration.intentNames.has(intent.name)) {
|
|
6513
|
+
throw new Error(
|
|
6514
|
+
`Duplicate auto/custom intent '${intent.name}' detected while generating ${op} task for table '${tableName}' in actor '${registration.actorName}'`
|
|
6515
|
+
);
|
|
6516
|
+
}
|
|
6517
|
+
registration.intentNames.add(intent.name);
|
|
6518
|
+
CadenzaService.defineIntent({
|
|
6519
|
+
name: intent.name,
|
|
6520
|
+
description: intent.description,
|
|
6521
|
+
input: intent.input
|
|
6522
|
+
});
|
|
6523
|
+
}
|
|
6524
|
+
task.respondsTo(...intents.map((intent) => intent.name));
|
|
6525
|
+
}
|
|
6526
|
+
validateOperationPayload(registration, operation, tableName, table, payload, options) {
|
|
6527
|
+
const allowedFields = new Set(Object.keys(table.fields));
|
|
6528
|
+
const resolvedMode = payload.queryMode ?? "rows";
|
|
6529
|
+
if (!["rows", "count", "exists", "one", "aggregate"].includes(resolvedMode)) {
|
|
6530
|
+
throw new Error(`Unsupported queryMode '${String(payload.queryMode)}'`);
|
|
6531
|
+
}
|
|
6532
|
+
const assertAllowedField = (fieldName, label) => {
|
|
6533
|
+
if (!allowedFields.has(fieldName)) {
|
|
6534
|
+
throw new Error(
|
|
6535
|
+
`Invalid field '${fieldName}' in ${label} for ${operation} on ${tableName}`
|
|
6536
|
+
);
|
|
6537
|
+
}
|
|
6538
|
+
};
|
|
6539
|
+
const aggregateDefinitions = Array.isArray(payload.aggregates) ? payload.aggregates : [];
|
|
6540
|
+
const aggregateSortAllowlist = /* @__PURE__ */ new Set();
|
|
6541
|
+
if (resolvedMode === "aggregate") {
|
|
6542
|
+
if (aggregateDefinitions.length === 0) {
|
|
6543
|
+
throw new Error(
|
|
6544
|
+
`Aggregate queryMode requires at least one aggregate on table '${tableName}'`
|
|
6545
|
+
);
|
|
6546
|
+
}
|
|
6547
|
+
for (const groupField of payload.groupBy ?? []) {
|
|
6548
|
+
assertAllowedField(groupField, "groupBy");
|
|
6549
|
+
aggregateSortAllowlist.add(groupField);
|
|
6550
|
+
}
|
|
6551
|
+
for (const [index, aggregate] of aggregateDefinitions.entries()) {
|
|
6552
|
+
if (!isSupportedAggregateFunction(aggregate.fn)) {
|
|
6553
|
+
throw new Error(
|
|
6554
|
+
`Unsupported aggregate function '${String(aggregate.fn)}' on table '${tableName}'`
|
|
6555
|
+
);
|
|
6556
|
+
}
|
|
6557
|
+
if (aggregate.fn !== "count" && !aggregate.field) {
|
|
6558
|
+
throw new Error(
|
|
6559
|
+
`Aggregate '${aggregate.fn}' requires field on table '${tableName}'`
|
|
6560
|
+
);
|
|
6561
|
+
}
|
|
6562
|
+
if (aggregate.field) {
|
|
6563
|
+
assertAllowedField(aggregate.field, "aggregates.field");
|
|
6564
|
+
}
|
|
6565
|
+
aggregateSortAllowlist.add(buildAggregateAlias(aggregate, index));
|
|
6566
|
+
}
|
|
6567
|
+
} else if (aggregateDefinitions.length > 0 || (payload.groupBy ?? []).length > 0) {
|
|
6568
|
+
throw new Error(
|
|
6569
|
+
`aggregates/groupBy payload requires queryMode='aggregate' on table '${tableName}'`
|
|
6570
|
+
);
|
|
6571
|
+
}
|
|
6572
|
+
if (options.enforceFieldAllowlist) {
|
|
6573
|
+
if (payload.fields) {
|
|
6574
|
+
for (const field of payload.fields) {
|
|
6575
|
+
assertAllowedField(field, "fields");
|
|
6576
|
+
}
|
|
6577
|
+
}
|
|
6578
|
+
if (payload.filter) {
|
|
6579
|
+
for (const field of Object.keys(payload.filter)) {
|
|
6580
|
+
assertAllowedField(field, "filter");
|
|
6581
|
+
}
|
|
6582
|
+
}
|
|
6583
|
+
if (payload.data) {
|
|
6584
|
+
const rows = resolveDataRows(payload.data);
|
|
6585
|
+
for (const row of rows) {
|
|
6586
|
+
for (const field of Object.keys(row)) {
|
|
6587
|
+
assertAllowedField(field, "data");
|
|
6588
|
+
}
|
|
6589
|
+
}
|
|
6590
|
+
}
|
|
6591
|
+
}
|
|
6592
|
+
if (payload.sort) {
|
|
6593
|
+
for (const field of Object.keys(payload.sort)) {
|
|
6594
|
+
if (resolvedMode === "aggregate" && aggregateSortAllowlist.has(field)) {
|
|
6595
|
+
continue;
|
|
6596
|
+
}
|
|
6597
|
+
assertAllowedField(field, "sort");
|
|
6598
|
+
}
|
|
6599
|
+
}
|
|
6600
|
+
if (payload.onConflict) {
|
|
6601
|
+
for (const conflictField of payload.onConflict.target ?? []) {
|
|
6602
|
+
assertAllowedField(conflictField, "onConflict.target");
|
|
6603
|
+
}
|
|
6604
|
+
for (const setField of Object.keys(payload.onConflict.action?.set ?? {})) {
|
|
6605
|
+
assertAllowedField(setField, "onConflict.action.set");
|
|
6606
|
+
}
|
|
6607
|
+
}
|
|
6608
|
+
if (payload.joins) {
|
|
6609
|
+
this.validateJoinPayload(registration.schema, payload.joins);
|
|
6610
|
+
}
|
|
6611
|
+
}
|
|
6612
|
+
validateJoinPayload(schema, joins) {
|
|
6613
|
+
for (const [joinTableName, joinDefinition] of Object.entries(joins)) {
|
|
6614
|
+
if (!schema.tables[joinTableName]) {
|
|
6615
|
+
throw new Error(`Invalid join table '${joinTableName}'. Table does not exist in schema.`);
|
|
6616
|
+
}
|
|
6617
|
+
const joinTable = schema.tables[joinTableName];
|
|
6618
|
+
for (const field of joinDefinition.fields ?? []) {
|
|
6619
|
+
if (!joinTable.fields[field]) {
|
|
6620
|
+
throw new Error(
|
|
6621
|
+
`Invalid join field '${field}' on joined table '${joinTableName}'`
|
|
6622
|
+
);
|
|
6623
|
+
}
|
|
6624
|
+
}
|
|
6625
|
+
if (joinDefinition.filter) {
|
|
6626
|
+
for (const filterField of Object.keys(joinDefinition.filter)) {
|
|
6627
|
+
if (!joinTable.fields[filterField]) {
|
|
6628
|
+
throw new Error(
|
|
6629
|
+
`Invalid join filter field '${filterField}' on joined table '${joinTableName}'`
|
|
6630
|
+
);
|
|
6631
|
+
}
|
|
6632
|
+
}
|
|
6633
|
+
}
|
|
6634
|
+
if (joinDefinition.joins) {
|
|
6635
|
+
this.validateJoinPayload(schema, joinDefinition.joins);
|
|
6636
|
+
}
|
|
6637
|
+
}
|
|
6638
|
+
}
|
|
6639
|
+
toCamelCase(rows) {
|
|
6640
|
+
return rows.map((row) => {
|
|
6641
|
+
const camelCasedRow = {};
|
|
6642
|
+
for (const [key, value] of Object.entries(row)) {
|
|
6643
|
+
camelCasedRow[camelCase(key)] = value;
|
|
6644
|
+
}
|
|
6645
|
+
return camelCasedRow;
|
|
6646
|
+
});
|
|
6647
|
+
}
|
|
6648
|
+
buildWhereClause(filter, params) {
|
|
6649
|
+
const conditions = [];
|
|
6650
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
6651
|
+
if (value !== void 0) {
|
|
6652
|
+
if (Array.isArray(value)) {
|
|
6653
|
+
const placeholders = value.map((entry) => {
|
|
6654
|
+
params.push(entry);
|
|
6655
|
+
return `$${params.length}`;
|
|
6656
|
+
}).join(", ");
|
|
6657
|
+
conditions.push(`${snakeCase(key)} IN (${placeholders})`);
|
|
6658
|
+
} else {
|
|
6659
|
+
params.push(value);
|
|
6660
|
+
conditions.push(`${snakeCase(key)} = $${params.length}`);
|
|
6661
|
+
}
|
|
6662
|
+
}
|
|
6663
|
+
}
|
|
6664
|
+
return conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
6665
|
+
}
|
|
6666
|
+
buildJoinClause(joins) {
|
|
6667
|
+
let joinSql = "";
|
|
6668
|
+
for (const [table, join] of Object.entries(joins)) {
|
|
6669
|
+
const alias = join.alias ? ` ${join.alias}` : "";
|
|
6670
|
+
joinSql += ` LEFT JOIN ${snakeCase(table)}${alias} ON ${join.on}`;
|
|
6671
|
+
if (join.joins) joinSql += " " + this.buildJoinClause(join.joins);
|
|
6672
|
+
}
|
|
6673
|
+
return joinSql;
|
|
6674
|
+
}
|
|
6675
|
+
async resolveNestedData(registration, data, tableName) {
|
|
6676
|
+
if (Array.isArray(data)) {
|
|
6677
|
+
return Promise.all(
|
|
6678
|
+
data.map((entry) => this.resolveNestedData(registration, entry, tableName))
|
|
6679
|
+
);
|
|
6680
|
+
}
|
|
6681
|
+
if (typeof data !== "object" || data === null) {
|
|
6682
|
+
return data;
|
|
6683
|
+
}
|
|
6684
|
+
const resolved = { ...data };
|
|
6685
|
+
for (const [key, value] of Object.entries(data)) {
|
|
6686
|
+
if (typeof value === "object" && value !== null && "subOperation" in value) {
|
|
6687
|
+
const subOperation = value;
|
|
6688
|
+
resolved[key] = await this.executeSubOperation(registration, subOperation);
|
|
6689
|
+
} else if (typeof value === "string" && ["increment", "decrement", "set"].includes(value)) {
|
|
6690
|
+
resolved[key] = { __effect: value };
|
|
6691
|
+
} else if (typeof value === "object" && value !== null) {
|
|
6692
|
+
resolved[key] = await this.resolveNestedData(registration, value, tableName);
|
|
6693
|
+
}
|
|
6694
|
+
}
|
|
6695
|
+
return resolved;
|
|
6696
|
+
}
|
|
6697
|
+
async executeSubOperation(registration, operation) {
|
|
6698
|
+
const targetTableName = operation.table;
|
|
6699
|
+
if (!registration.schema.tables[targetTableName]) {
|
|
6700
|
+
throw new Error(
|
|
6701
|
+
`Sub-operation table '${targetTableName}' does not exist in actor schema`
|
|
6403
6702
|
);
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6703
|
+
}
|
|
6704
|
+
const pool = this.getPoolOrThrow(registration);
|
|
6705
|
+
const safetyPolicy = this.resolveSafetyPolicy(registration);
|
|
6706
|
+
return this.executeWithTransaction(pool, true, async (client) => {
|
|
6707
|
+
if (operation.subOperation === "insert") {
|
|
6708
|
+
const resolvedData = await this.resolveNestedData(
|
|
6709
|
+
registration,
|
|
6710
|
+
operation.data,
|
|
6711
|
+
operation.table
|
|
6412
6712
|
);
|
|
6713
|
+
const row = ensurePlainObject(resolvedData, "sub-operation insert data");
|
|
6714
|
+
const keys = Object.keys(row);
|
|
6715
|
+
const params = Object.values(row);
|
|
6716
|
+
const sql2 = `INSERT INTO ${operation.table} (${keys.map((key) => snakeCase(key)).join(", ")}) VALUES (${params.map((_, index) => `$${index + 1}`).join(", ")}) ON CONFLICT DO NOTHING RETURNING ${operation.return ?? "*"}`;
|
|
6717
|
+
const result2 = await this.withTimeout(
|
|
6718
|
+
() => client.query(sql2, params),
|
|
6719
|
+
safetyPolicy.statementTimeoutMs,
|
|
6720
|
+
`Sub-operation insert timeout on table ${operation.table}`
|
|
6721
|
+
);
|
|
6722
|
+
const returnKey2 = operation.return ?? "uuid";
|
|
6723
|
+
if (result2.rows[0]?.[returnKey2] !== void 0) {
|
|
6724
|
+
return result2.rows[0][returnKey2];
|
|
6725
|
+
}
|
|
6726
|
+
return row[returnKey2] ?? row.uuid ?? {};
|
|
6413
6727
|
}
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6728
|
+
const queryParams = [];
|
|
6729
|
+
const whereClause = this.buildWhereClause(operation.filter || {}, queryParams);
|
|
6730
|
+
const sql = `SELECT ${(operation.fields ?? ["*"]).map((field) => field === "*" ? field : snakeCase(field)).join(", ")} FROM ${operation.table} ${whereClause} LIMIT 1`;
|
|
6731
|
+
const result = await this.withTimeout(
|
|
6732
|
+
() => client.query(sql, queryParams),
|
|
6733
|
+
safetyPolicy.statementTimeoutMs,
|
|
6734
|
+
`Sub-operation query timeout on table ${operation.table}`
|
|
6735
|
+
);
|
|
6736
|
+
const returnKey = operation.return ?? "uuid";
|
|
6737
|
+
return result.rows[0]?.[returnKey] ?? {};
|
|
6738
|
+
});
|
|
6423
6739
|
}
|
|
6424
6740
|
getInputSchema(op, tableName, table) {
|
|
6425
6741
|
const inputSchema = {
|
|
@@ -6438,66 +6754,43 @@ var DatabaseController = class _DatabaseController {
|
|
|
6438
6754
|
}
|
|
6439
6755
|
inputSchema.properties.transaction = getTransactionSchema();
|
|
6440
6756
|
inputSchema.properties.queryData.properties.transaction = inputSchema.properties.transaction;
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
inputSchema.properties.queryData.properties.limit = inputSchema.properties.limit;
|
|
6479
|
-
inputSchema.properties.offset = getQueryOffsetSchemaFromTable();
|
|
6480
|
-
inputSchema.properties.queryData.properties.offset = inputSchema.properties.offset;
|
|
6481
|
-
break;
|
|
6482
|
-
case "update":
|
|
6483
|
-
inputSchema.properties.filter = getQueryFilterSchemaFromTable(
|
|
6484
|
-
table,
|
|
6485
|
-
tableName
|
|
6486
|
-
);
|
|
6487
|
-
inputSchema.properties.queryData.properties.filter = inputSchema.properties.filter;
|
|
6488
|
-
inputSchema.properties.fields = getQueryFieldsSchemaFromTable(
|
|
6489
|
-
table,
|
|
6490
|
-
tableName
|
|
6491
|
-
);
|
|
6492
|
-
inputSchema.properties.queryData.properties.fields = inputSchema.properties.fields;
|
|
6493
|
-
break;
|
|
6494
|
-
case "delete":
|
|
6495
|
-
inputSchema.properties.filter = getQueryFilterSchemaFromTable(
|
|
6496
|
-
table,
|
|
6497
|
-
tableName
|
|
6498
|
-
);
|
|
6499
|
-
inputSchema.properties.queryData.properties.filter = inputSchema.properties.filter;
|
|
6500
|
-
break;
|
|
6757
|
+
if (op === "insert" || op === "update") {
|
|
6758
|
+
inputSchema.properties.data = getInsertDataSchemaFromTable(table, tableName);
|
|
6759
|
+
inputSchema.properties.queryData.properties.data = inputSchema.properties.data;
|
|
6760
|
+
}
|
|
6761
|
+
if (op === "insert") {
|
|
6762
|
+
inputSchema.properties.batch = getQueryBatchSchemaFromTable();
|
|
6763
|
+
inputSchema.properties.queryData.properties.batch = inputSchema.properties.batch;
|
|
6764
|
+
inputSchema.properties.onConflict = getQueryOnConflictSchemaFromTable(
|
|
6765
|
+
table,
|
|
6766
|
+
tableName
|
|
6767
|
+
);
|
|
6768
|
+
inputSchema.properties.queryData.properties.onConflict = inputSchema.properties.onConflict;
|
|
6769
|
+
}
|
|
6770
|
+
if (op === "query" || op === "update" || op === "delete") {
|
|
6771
|
+
inputSchema.properties.filter = getQueryFilterSchemaFromTable(table, tableName);
|
|
6772
|
+
inputSchema.properties.queryData.properties.filter = inputSchema.properties.filter;
|
|
6773
|
+
}
|
|
6774
|
+
if (op === "query") {
|
|
6775
|
+
inputSchema.properties.queryMode = getQueryModeSchema();
|
|
6776
|
+
inputSchema.properties.queryData.properties.queryMode = inputSchema.properties.queryMode;
|
|
6777
|
+
inputSchema.properties.fields = getQueryFieldsSchemaFromTable(table, tableName);
|
|
6778
|
+
inputSchema.properties.queryData.properties.fields = inputSchema.properties.fields;
|
|
6779
|
+
inputSchema.properties.joins = getQueryJoinsSchemaFromTable(table, tableName);
|
|
6780
|
+
inputSchema.properties.queryData.properties.joins = inputSchema.properties.joins;
|
|
6781
|
+
inputSchema.properties.sort = getQuerySortSchemaFromTable(table, tableName);
|
|
6782
|
+
inputSchema.properties.queryData.properties.sort = inputSchema.properties.sort;
|
|
6783
|
+
inputSchema.properties.aggregates = getQueryAggregatesSchemaFromTable(
|
|
6784
|
+
table,
|
|
6785
|
+
tableName
|
|
6786
|
+
);
|
|
6787
|
+
inputSchema.properties.queryData.properties.aggregates = inputSchema.properties.aggregates;
|
|
6788
|
+
inputSchema.properties.groupBy = getQueryGroupBySchemaFromTable(table, tableName);
|
|
6789
|
+
inputSchema.properties.queryData.properties.groupBy = inputSchema.properties.groupBy;
|
|
6790
|
+
inputSchema.properties.limit = getQueryLimitSchemaFromTable();
|
|
6791
|
+
inputSchema.properties.queryData.properties.limit = inputSchema.properties.limit;
|
|
6792
|
+
inputSchema.properties.offset = getQueryOffsetSchemaFromTable();
|
|
6793
|
+
inputSchema.properties.queryData.properties.offset = inputSchema.properties.offset;
|
|
6501
6794
|
}
|
|
6502
6795
|
return inputSchema;
|
|
6503
6796
|
}
|
|
@@ -6507,13 +6800,13 @@ function getInsertDataSchemaFromTable(table, tableName) {
|
|
|
6507
6800
|
type: "object",
|
|
6508
6801
|
properties: {
|
|
6509
6802
|
...Object.fromEntries(
|
|
6510
|
-
Object.entries(table.fields).map((field) => {
|
|
6803
|
+
Object.entries(table.fields).map(([fieldName, field]) => {
|
|
6511
6804
|
return [
|
|
6512
|
-
|
|
6805
|
+
fieldName,
|
|
6513
6806
|
{
|
|
6514
6807
|
value: {
|
|
6515
|
-
type: tableFieldTypeToSchemaType(field
|
|
6516
|
-
description: `Inferred from field '${
|
|
6808
|
+
type: tableFieldTypeToSchemaType(field.type),
|
|
6809
|
+
description: `Inferred from field '${fieldName}' of type [${field.type}] on table ${tableName}.`
|
|
6517
6810
|
},
|
|
6518
6811
|
effect: {
|
|
6519
6812
|
type: "string",
|
|
@@ -6562,7 +6855,7 @@ function getInsertDataSchemaFromTable(table, tableName) {
|
|
|
6562
6855
|
})
|
|
6563
6856
|
)
|
|
6564
6857
|
},
|
|
6565
|
-
required: Object.entries(table.fields).filter((field) => field
|
|
6858
|
+
required: Object.entries(table.fields).filter(([, field]) => field.required || field.primary).map(([fieldName]) => fieldName),
|
|
6566
6859
|
strict: true
|
|
6567
6860
|
};
|
|
6568
6861
|
return {
|
|
@@ -6578,18 +6871,18 @@ function getQueryFilterSchemaFromTable(table, tableName) {
|
|
|
6578
6871
|
type: "object",
|
|
6579
6872
|
properties: {
|
|
6580
6873
|
...Object.fromEntries(
|
|
6581
|
-
Object.entries(table.fields).map((field) => {
|
|
6874
|
+
Object.entries(table.fields).map(([fieldName, field]) => {
|
|
6582
6875
|
return [
|
|
6583
|
-
|
|
6876
|
+
fieldName,
|
|
6584
6877
|
{
|
|
6585
6878
|
value: {
|
|
6586
|
-
type: tableFieldTypeToSchemaType(field
|
|
6587
|
-
description: `Inferred from field '${
|
|
6879
|
+
type: tableFieldTypeToSchemaType(field.type),
|
|
6880
|
+
description: `Inferred from field '${fieldName}' of type [${field.type}] on table ${tableName}.`
|
|
6588
6881
|
},
|
|
6589
6882
|
in: {
|
|
6590
6883
|
type: "array",
|
|
6591
6884
|
items: {
|
|
6592
|
-
type: tableFieldTypeToSchemaType(field
|
|
6885
|
+
type: tableFieldTypeToSchemaType(field.type)
|
|
6593
6886
|
}
|
|
6594
6887
|
}
|
|
6595
6888
|
}
|
|
@@ -6598,7 +6891,7 @@ function getQueryFilterSchemaFromTable(table, tableName) {
|
|
|
6598
6891
|
)
|
|
6599
6892
|
},
|
|
6600
6893
|
strict: true,
|
|
6601
|
-
description: `Inferred from table '${tableName}' on
|
|
6894
|
+
description: `Inferred from table '${tableName}' on postgres actor table contract.`
|
|
6602
6895
|
};
|
|
6603
6896
|
}
|
|
6604
6897
|
function getQueryFieldsSchemaFromTable(table, tableName) {
|
|
@@ -6610,106 +6903,100 @@ function getQueryFieldsSchemaFromTable(table, tableName) {
|
|
|
6610
6903
|
oneOf: Object.keys(table.fields)
|
|
6611
6904
|
}
|
|
6612
6905
|
},
|
|
6613
|
-
description: `Inferred from table '${tableName}'
|
|
6906
|
+
description: `Inferred field projection from table '${tableName}'.`
|
|
6614
6907
|
};
|
|
6615
6908
|
}
|
|
6616
|
-
function
|
|
6909
|
+
function getQueryModeSchema() {
|
|
6617
6910
|
return {
|
|
6618
|
-
type: "
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
},
|
|
6652
|
-
required: ["on", "fields"],
|
|
6653
|
-
strict: true
|
|
6654
|
-
}
|
|
6655
|
-
];
|
|
6656
|
-
})
|
|
6657
|
-
)
|
|
6911
|
+
type: "string",
|
|
6912
|
+
constraints: {
|
|
6913
|
+
oneOf: ["rows", "count", "exists", "one", "aggregate"]
|
|
6914
|
+
}
|
|
6915
|
+
};
|
|
6916
|
+
}
|
|
6917
|
+
function getQueryAggregatesSchemaFromTable(table, tableName) {
|
|
6918
|
+
return {
|
|
6919
|
+
type: "array",
|
|
6920
|
+
items: {
|
|
6921
|
+
type: "object",
|
|
6922
|
+
properties: {
|
|
6923
|
+
fn: {
|
|
6924
|
+
type: "string",
|
|
6925
|
+
constraints: {
|
|
6926
|
+
oneOf: ["count", "sum", "avg", "min", "max"]
|
|
6927
|
+
}
|
|
6928
|
+
},
|
|
6929
|
+
field: {
|
|
6930
|
+
type: "string",
|
|
6931
|
+
constraints: {
|
|
6932
|
+
oneOf: Object.keys(table.fields)
|
|
6933
|
+
}
|
|
6934
|
+
},
|
|
6935
|
+
as: {
|
|
6936
|
+
type: "string"
|
|
6937
|
+
},
|
|
6938
|
+
distinct: {
|
|
6939
|
+
type: "boolean"
|
|
6940
|
+
}
|
|
6941
|
+
},
|
|
6942
|
+
required: ["fn"],
|
|
6943
|
+
strict: true
|
|
6658
6944
|
},
|
|
6659
|
-
|
|
6660
|
-
description: `Inferred from table '${tableName}' on database service ${CadenzaService.serviceRegistry?.serviceName ?? "unknown-service"}.`
|
|
6945
|
+
description: `Aggregate definitions inferred from table '${tableName}'.`
|
|
6661
6946
|
};
|
|
6662
6947
|
}
|
|
6663
|
-
function
|
|
6948
|
+
function getQueryGroupBySchemaFromTable(table, tableName) {
|
|
6664
6949
|
return {
|
|
6665
|
-
type: "
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
{
|
|
6672
|
-
type: "string",
|
|
6673
|
-
constraints: {
|
|
6674
|
-
oneOf: ["asc", "desc"]
|
|
6675
|
-
}
|
|
6676
|
-
}
|
|
6677
|
-
];
|
|
6678
|
-
})
|
|
6679
|
-
)
|
|
6950
|
+
type: "array",
|
|
6951
|
+
items: {
|
|
6952
|
+
type: "string",
|
|
6953
|
+
constraints: {
|
|
6954
|
+
oneOf: Object.keys(table.fields)
|
|
6955
|
+
}
|
|
6680
6956
|
},
|
|
6681
|
-
|
|
6682
|
-
|
|
6957
|
+
description: `Group by fields inferred from table '${tableName}'.`
|
|
6958
|
+
};
|
|
6959
|
+
}
|
|
6960
|
+
function getQueryJoinsSchemaFromTable(_table, tableName) {
|
|
6961
|
+
return {
|
|
6962
|
+
type: "object",
|
|
6963
|
+
description: `Join definitions for table '${tableName}'.`
|
|
6964
|
+
};
|
|
6965
|
+
}
|
|
6966
|
+
function getQuerySortSchemaFromTable(_table, tableName) {
|
|
6967
|
+
return {
|
|
6968
|
+
type: "object",
|
|
6969
|
+
strict: false,
|
|
6970
|
+
description: `Sort definition for table '${tableName}'. Keys are validated at runtime against allowlists and aggregate aliases.`
|
|
6683
6971
|
};
|
|
6684
6972
|
}
|
|
6685
6973
|
function getQueryLimitSchemaFromTable() {
|
|
6686
6974
|
return {
|
|
6687
6975
|
type: "number",
|
|
6688
6976
|
constraints: {
|
|
6689
|
-
min:
|
|
6690
|
-
|
|
6691
|
-
|
|
6977
|
+
min: 0,
|
|
6978
|
+
max: 1e3
|
|
6979
|
+
}
|
|
6692
6980
|
};
|
|
6693
6981
|
}
|
|
6694
6982
|
function getQueryOffsetSchemaFromTable() {
|
|
6695
6983
|
return {
|
|
6696
6984
|
type: "number",
|
|
6697
6985
|
constraints: {
|
|
6698
|
-
min: 0
|
|
6699
|
-
|
|
6700
|
-
|
|
6986
|
+
min: 0,
|
|
6987
|
+
max: 1e6
|
|
6988
|
+
}
|
|
6701
6989
|
};
|
|
6702
6990
|
}
|
|
6703
|
-
function
|
|
6991
|
+
function getQueryBatchSchemaFromTable() {
|
|
6704
6992
|
return {
|
|
6705
|
-
type: "boolean"
|
|
6706
|
-
description: "Whether to run the query in a transaction"
|
|
6993
|
+
type: "boolean"
|
|
6707
6994
|
};
|
|
6708
6995
|
}
|
|
6709
|
-
function
|
|
6996
|
+
function getTransactionSchema() {
|
|
6710
6997
|
return {
|
|
6711
6998
|
type: "boolean",
|
|
6712
|
-
description: "
|
|
6999
|
+
description: "Execute the operation in a transaction."
|
|
6713
7000
|
};
|
|
6714
7001
|
}
|
|
6715
7002
|
function getQueryOnConflictSchemaFromTable(table, tableName) {
|
|
@@ -6735,55 +7022,43 @@ function getQueryOnConflictSchemaFromTable(table, tableName) {
|
|
|
6735
7022
|
}
|
|
6736
7023
|
},
|
|
6737
7024
|
set: {
|
|
6738
|
-
type: "object"
|
|
6739
|
-
properties: {
|
|
6740
|
-
...Object.fromEntries(
|
|
6741
|
-
Object.entries(table.fields).map((field) => {
|
|
6742
|
-
return [
|
|
6743
|
-
field[0],
|
|
6744
|
-
{
|
|
6745
|
-
type: tableFieldTypeToSchemaType(field[1].type),
|
|
6746
|
-
description: `Inferred from field '${field[0]}' of type [${field[1].type}] on table ${tableName}.`
|
|
6747
|
-
}
|
|
6748
|
-
];
|
|
6749
|
-
})
|
|
6750
|
-
)
|
|
6751
|
-
}
|
|
7025
|
+
type: "object"
|
|
6752
7026
|
},
|
|
6753
7027
|
where: {
|
|
6754
7028
|
type: "string"
|
|
6755
7029
|
}
|
|
6756
|
-
}
|
|
6757
|
-
required: ["do"]
|
|
7030
|
+
}
|
|
6758
7031
|
}
|
|
6759
7032
|
},
|
|
6760
|
-
|
|
6761
|
-
|
|
7033
|
+
strict: true,
|
|
7034
|
+
description: `Conflict strategy for inserts on table '${tableName}'.`
|
|
6762
7035
|
};
|
|
6763
7036
|
}
|
|
6764
7037
|
function tableFieldTypeToSchemaType(type) {
|
|
6765
7038
|
switch (type) {
|
|
6766
7039
|
case "varchar":
|
|
6767
7040
|
case "text":
|
|
6768
|
-
case "jsonb":
|
|
6769
7041
|
case "uuid":
|
|
7042
|
+
case "timestamp":
|
|
6770
7043
|
case "date":
|
|
6771
7044
|
case "geo_point":
|
|
6772
|
-
case "bytea":
|
|
6773
7045
|
return "string";
|
|
6774
7046
|
case "int":
|
|
6775
7047
|
case "bigint":
|
|
6776
7048
|
case "decimal":
|
|
6777
|
-
case "timestamp":
|
|
6778
7049
|
return "number";
|
|
6779
7050
|
case "boolean":
|
|
6780
7051
|
return "boolean";
|
|
6781
7052
|
case "array":
|
|
6782
7053
|
return "array";
|
|
6783
7054
|
case "object":
|
|
7055
|
+
case "jsonb":
|
|
6784
7056
|
return "object";
|
|
7057
|
+
case "bytea":
|
|
7058
|
+
return "string";
|
|
7059
|
+
default:
|
|
7060
|
+
return "any";
|
|
6785
7061
|
}
|
|
6786
|
-
return "any";
|
|
6787
7062
|
}
|
|
6788
7063
|
|
|
6789
7064
|
// src/Cadenza.ts
|
|
@@ -8359,14 +8634,61 @@ var CadenzaService = class {
|
|
|
8359
8634
|
this.createCadenzaService(serviceName, description, options);
|
|
8360
8635
|
}
|
|
8361
8636
|
/**
|
|
8362
|
-
* Creates and initializes a
|
|
8363
|
-
* This
|
|
8637
|
+
* Creates and initializes a specialized PostgresActor.
|
|
8638
|
+
* This is actor-only and does not create or register a network service.
|
|
8364
8639
|
*
|
|
8365
|
-
* @param {string} name -
|
|
8366
|
-
* @param {DatabaseSchemaDefinition} schema -
|
|
8367
|
-
* @param {string} [description=""] -
|
|
8368
|
-
* @param {ServerOptions & DatabaseOptions} [options={}] -
|
|
8369
|
-
* @return {void}
|
|
8640
|
+
* @param {string} name - Logical PostgresActor name.
|
|
8641
|
+
* @param {DatabaseSchemaDefinition} schema - Database schema definition.
|
|
8642
|
+
* @param {string} [description=""] - Optional human-readable actor description.
|
|
8643
|
+
* @param {ServerOptions & DatabaseOptions} [options={}] - Actor/database runtime options.
|
|
8644
|
+
* @return {void}
|
|
8645
|
+
*/
|
|
8646
|
+
static createPostgresActor(name, schema, description = "", options = {}) {
|
|
8647
|
+
if (isBrowser) {
|
|
8648
|
+
console.warn(
|
|
8649
|
+
"PostgresActor creation is not supported in the browser."
|
|
8650
|
+
);
|
|
8651
|
+
return;
|
|
8652
|
+
}
|
|
8653
|
+
this.bootstrap();
|
|
8654
|
+
this.validateName(name);
|
|
8655
|
+
const databaseController = DatabaseController.instance;
|
|
8656
|
+
const normalizedOptions = this.normalizePostgresActorOptions(name, options);
|
|
8657
|
+
const registration = databaseController.createPostgresActor(
|
|
8658
|
+
name,
|
|
8659
|
+
schema,
|
|
8660
|
+
description,
|
|
8661
|
+
normalizedOptions
|
|
8662
|
+
);
|
|
8663
|
+
console.log("Creating PostgresActor", {
|
|
8664
|
+
name,
|
|
8665
|
+
actorName: registration.actorName,
|
|
8666
|
+
ownerServiceName: normalizedOptions.ownerServiceName ?? null,
|
|
8667
|
+
options: normalizedOptions
|
|
8668
|
+
});
|
|
8669
|
+
databaseController.requestPostgresActorSetup(registration, {
|
|
8670
|
+
actorName: registration.actorName,
|
|
8671
|
+
actorToken: registration.actorToken,
|
|
8672
|
+
databaseName: normalizedOptions.databaseName,
|
|
8673
|
+
ownerServiceName: normalizedOptions.ownerServiceName ?? null
|
|
8674
|
+
});
|
|
8675
|
+
}
|
|
8676
|
+
/**
|
|
8677
|
+
* Creates a meta PostgresActor.
|
|
8678
|
+
*
|
|
8679
|
+
* @param {string} name - Logical PostgresActor name.
|
|
8680
|
+
* @param {DatabaseSchemaDefinition} schema - Database schema definition.
|
|
8681
|
+
* @param {string} [description=""] - Optional description.
|
|
8682
|
+
* @param {ServerOptions & DatabaseOptions} [options={}] - Optional actor/database options.
|
|
8683
|
+
* @return {void}
|
|
8684
|
+
*/
|
|
8685
|
+
static createMetaPostgresActor(name, schema, description = "", options = {}) {
|
|
8686
|
+
this.bootstrap();
|
|
8687
|
+
options.isMeta = true;
|
|
8688
|
+
this.createPostgresActor(name, schema, description, options);
|
|
8689
|
+
}
|
|
8690
|
+
/**
|
|
8691
|
+
* Creates a dedicated database service by composing a PostgresActor and a Cadenza service.
|
|
8370
8692
|
*/
|
|
8371
8693
|
static createDatabaseService(name, schema, description = "", options = {}) {
|
|
8372
8694
|
if (isBrowser) {
|
|
@@ -8377,9 +8699,102 @@ var CadenzaService = class {
|
|
|
8377
8699
|
}
|
|
8378
8700
|
if (this.serviceCreated) return;
|
|
8379
8701
|
this.bootstrap();
|
|
8380
|
-
this.
|
|
8381
|
-
|
|
8382
|
-
|
|
8702
|
+
this.validateName(name);
|
|
8703
|
+
this.validateServiceName(name);
|
|
8704
|
+
const databaseController = DatabaseController.instance;
|
|
8705
|
+
const actorOptions = this.normalizePostgresActorOptions(name, {
|
|
8706
|
+
...options,
|
|
8707
|
+
ownerServiceName: options.ownerServiceName ?? name
|
|
8708
|
+
});
|
|
8709
|
+
const serviceOptions = this.normalizeDatabaseServiceOptions(name, {
|
|
8710
|
+
...options,
|
|
8711
|
+
ownerServiceName: actorOptions.ownerServiceName
|
|
8712
|
+
});
|
|
8713
|
+
const registration = databaseController.createPostgresActor(
|
|
8714
|
+
name,
|
|
8715
|
+
schema,
|
|
8716
|
+
description,
|
|
8717
|
+
actorOptions
|
|
8718
|
+
);
|
|
8719
|
+
this.registerDatabaseServiceBridgeTask(
|
|
8720
|
+
name,
|
|
8721
|
+
description,
|
|
8722
|
+
schema,
|
|
8723
|
+
Boolean(serviceOptions.isMeta),
|
|
8724
|
+
registration.actorName
|
|
8725
|
+
);
|
|
8726
|
+
const createServiceTaskName = `Create database service ${name} after ${registration.actorName} setup`;
|
|
8727
|
+
if (!this.get(createServiceTaskName)) {
|
|
8728
|
+
this.createMetaTask(
|
|
8729
|
+
createServiceTaskName,
|
|
8730
|
+
() => {
|
|
8731
|
+
this.createCadenzaService(name, description, serviceOptions);
|
|
8732
|
+
return true;
|
|
8733
|
+
},
|
|
8734
|
+
"Creates the networked database service after PostgresActor setup completes."
|
|
8735
|
+
).doOn(registration.setupDoneSignal);
|
|
8736
|
+
}
|
|
8737
|
+
const setupFailureTaskName = `Handle database service ${name} bootstrap failure`;
|
|
8738
|
+
if (!this.get(setupFailureTaskName)) {
|
|
8739
|
+
this.createMetaTask(
|
|
8740
|
+
setupFailureTaskName,
|
|
8741
|
+
(ctx) => {
|
|
8742
|
+
this.log(
|
|
8743
|
+
"Database service bootstrap failed before service creation.",
|
|
8744
|
+
{
|
|
8745
|
+
serviceName: name,
|
|
8746
|
+
actorName: registration.actorName,
|
|
8747
|
+
databaseName: registration.databaseName,
|
|
8748
|
+
error: ctx.__error
|
|
8749
|
+
},
|
|
8750
|
+
"error"
|
|
8751
|
+
);
|
|
8752
|
+
return true;
|
|
8753
|
+
},
|
|
8754
|
+
"Logs PostgresActor setup failures for database service bootstrap."
|
|
8755
|
+
).doOn(registration.setupFailedSignal);
|
|
8756
|
+
}
|
|
8757
|
+
console.log("Creating database service wrapper", {
|
|
8758
|
+
serviceName: name,
|
|
8759
|
+
actorName: registration.actorName,
|
|
8760
|
+
actorOptions,
|
|
8761
|
+
serviceOptions
|
|
8762
|
+
});
|
|
8763
|
+
databaseController.requestPostgresActorSetup(registration, {
|
|
8764
|
+
actorName: registration.actorName,
|
|
8765
|
+
actorToken: registration.actorToken,
|
|
8766
|
+
databaseName: actorOptions.databaseName,
|
|
8767
|
+
ownerServiceName: actorOptions.ownerServiceName ?? name
|
|
8768
|
+
});
|
|
8769
|
+
}
|
|
8770
|
+
/**
|
|
8771
|
+
* Creates a meta database service with the specified configuration.
|
|
8772
|
+
*
|
|
8773
|
+
* @param {string} name - The name of the database service to be created.
|
|
8774
|
+
* @param {DatabaseSchemaDefinition} schema - The schema definition for the database.
|
|
8775
|
+
* @param {string} [description=""] - An optional description of the database service.
|
|
8776
|
+
* @param {ServerOptions & DatabaseOptions} [options={}] - Optional server and database configuration options. The `isMeta` flag will be automatically set to true.
|
|
8777
|
+
* @return {void} - This method does not return a value.
|
|
8778
|
+
*/
|
|
8779
|
+
static createMetaDatabaseService(name, schema, description = "", options = {}) {
|
|
8780
|
+
this.createDatabaseService(name, schema, description, {
|
|
8781
|
+
...options,
|
|
8782
|
+
isMeta: true
|
|
8783
|
+
});
|
|
8784
|
+
}
|
|
8785
|
+
static normalizePostgresActorOptions(name, options = {}) {
|
|
8786
|
+
return {
|
|
8787
|
+
isMeta: false,
|
|
8788
|
+
retryCount: 3,
|
|
8789
|
+
databaseType: "postgres",
|
|
8790
|
+
databaseName: snakeCase2(name),
|
|
8791
|
+
poolSize: parseInt(process.env.DATABASE_POOL_SIZE ?? "10"),
|
|
8792
|
+
ownerServiceName: options.ownerServiceName ?? this.serviceRegistry?.serviceName ?? null,
|
|
8793
|
+
...options
|
|
8794
|
+
};
|
|
8795
|
+
}
|
|
8796
|
+
static normalizeDatabaseServiceOptions(name, options = {}) {
|
|
8797
|
+
return {
|
|
8383
8798
|
loadBalance: true,
|
|
8384
8799
|
useSocket: true,
|
|
8385
8800
|
displayName: void 0,
|
|
@@ -8397,45 +8812,38 @@ var CadenzaService = class {
|
|
|
8397
8812
|
databaseName: snakeCase2(name),
|
|
8398
8813
|
poolSize: parseInt(process.env.DATABASE_POOL_SIZE ?? "10"),
|
|
8399
8814
|
isDatabase: true,
|
|
8815
|
+
ownerServiceName: options.ownerServiceName ?? name,
|
|
8400
8816
|
...options
|
|
8401
8817
|
};
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
}
|
|
8408
|
-
this.createMetaTask(
|
|
8409
|
-
|
|
8818
|
+
}
|
|
8819
|
+
static registerDatabaseServiceBridgeTask(serviceName, description, schema, isMeta, actorName) {
|
|
8820
|
+
const taskName = `Insert database service bridge ${serviceName}`;
|
|
8821
|
+
if (this.get(taskName)) {
|
|
8822
|
+
return;
|
|
8823
|
+
}
|
|
8824
|
+
this.createMetaTask(
|
|
8825
|
+
taskName,
|
|
8826
|
+
(ctx, emit) => {
|
|
8827
|
+
if (ctx.__serviceName && ctx.__serviceName !== serviceName) {
|
|
8828
|
+
return false;
|
|
8829
|
+
}
|
|
8410
8830
|
emit("global.meta.created_database_service", {
|
|
8411
8831
|
data: {
|
|
8412
|
-
service_name:
|
|
8832
|
+
service_name: serviceName,
|
|
8413
8833
|
description,
|
|
8414
8834
|
schema,
|
|
8415
|
-
is_meta:
|
|
8835
|
+
is_meta: isMeta
|
|
8416
8836
|
}
|
|
8417
8837
|
});
|
|
8418
8838
|
this.log("Database service created", {
|
|
8419
|
-
name,
|
|
8420
|
-
isMeta
|
|
8839
|
+
name: serviceName,
|
|
8840
|
+
isMeta,
|
|
8841
|
+
actorName
|
|
8421
8842
|
});
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
/**
|
|
8427
|
-
* Creates a meta database service with the specified configuration.
|
|
8428
|
-
*
|
|
8429
|
-
* @param {string} name - The name of the database service to be created.
|
|
8430
|
-
* @param {DatabaseSchemaDefinition} schema - The schema definition for the database.
|
|
8431
|
-
* @param {string} [description=""] - An optional description of the database service.
|
|
8432
|
-
* @param {ServerOptions & DatabaseOptions} [options={}] - Optional server and database configuration options. The `isMeta` flag will be automatically set to true.
|
|
8433
|
-
* @return {void} - This method does not return a value.
|
|
8434
|
-
*/
|
|
8435
|
-
static createMetaDatabaseService(name, schema, description = "", options = {}) {
|
|
8436
|
-
this.bootstrap();
|
|
8437
|
-
options.isMeta = true;
|
|
8438
|
-
this.createDatabaseService(name, schema, description, options);
|
|
8843
|
+
return true;
|
|
8844
|
+
},
|
|
8845
|
+
"Bridges database service creation into the global metadata signal."
|
|
8846
|
+
).doOn("meta.service_registry.service_inserted");
|
|
8439
8847
|
}
|
|
8440
8848
|
static createActor(spec, options = {}) {
|
|
8441
8849
|
this.bootstrap();
|
|
@@ -8788,14 +9196,14 @@ var CadenzaService = class {
|
|
|
8788
9196
|
return Cadenza.createEphemeralTask(name, func, description, options);
|
|
8789
9197
|
}
|
|
8790
9198
|
/**
|
|
8791
|
-
* Creates an ephemeral meta
|
|
9199
|
+
* Creates an ephemeral meta-task with the specified name, function, description, and options.
|
|
8792
9200
|
* See {@link createEphemeralTask} and {@link createMetaTask} for more details.
|
|
8793
9201
|
*
|
|
8794
9202
|
* @param {string} name - The name of the task to be created.
|
|
8795
9203
|
* @param {TaskFunction} func - The function to be executed as part of the task.
|
|
8796
9204
|
* @param {string} [description] - An optional description of the task.
|
|
8797
9205
|
* @param {TaskOptions & EphemeralTaskOptions} [options={}] - Additional options for configuring the task.
|
|
8798
|
-
* @return {EphemeralTask} The created ephemeral meta
|
|
9206
|
+
* @return {EphemeralTask} The created ephemeral meta-task.
|
|
8799
9207
|
*/
|
|
8800
9208
|
static createEphemeralMetaTask(name, func, description, options = {}) {
|
|
8801
9209
|
this.bootstrap();
|