@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.js
CHANGED
|
@@ -1698,7 +1698,7 @@ var ServiceRegistry = class _ServiceRegistry {
|
|
|
1698
1698
|
}
|
|
1699
1699
|
).emits("meta.service_registry.service_inserted").emitsOnFail("meta.service_registry.service_insertion_failed");
|
|
1700
1700
|
this.insertServiceInstanceTask = CadenzaService.createCadenzaDBInsertTask(
|
|
1701
|
-
"
|
|
1701
|
+
"service_instance",
|
|
1702
1702
|
{},
|
|
1703
1703
|
{
|
|
1704
1704
|
inputSchema: {
|
|
@@ -2642,20 +2642,44 @@ var RestController = class _RestController {
|
|
|
2642
2642
|
let ctx2;
|
|
2643
2643
|
ctx2 = req.body;
|
|
2644
2644
|
deputyExecId = ctx2.__metadata.__deputyExecId;
|
|
2645
|
+
const remoteRoutineName = ctx2.__remoteRoutineName;
|
|
2646
|
+
const targetNotFoundSignal = `meta.rest.delegation_target_not_found:${deputyExecId}`;
|
|
2647
|
+
let resolved = false;
|
|
2648
|
+
const resolveDelegation = (endCtx, status) => {
|
|
2649
|
+
if (resolved || res.headersSent) {
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
2652
|
+
resolved = true;
|
|
2653
|
+
const metadata = endCtx?.__metadata && typeof endCtx.__metadata === "object" ? endCtx.__metadata : {};
|
|
2654
|
+
if (endCtx?.__metadata) {
|
|
2655
|
+
delete endCtx.__metadata;
|
|
2656
|
+
}
|
|
2657
|
+
res.json({
|
|
2658
|
+
...endCtx,
|
|
2659
|
+
...metadata,
|
|
2660
|
+
__status: status
|
|
2661
|
+
});
|
|
2662
|
+
};
|
|
2645
2663
|
CadenzaService.createEphemeralMetaTask(
|
|
2646
2664
|
"Resolve delegation",
|
|
2647
|
-
(endCtx) =>
|
|
2648
|
-
const metadata = endCtx.__metadata;
|
|
2649
|
-
delete endCtx.__metadata;
|
|
2650
|
-
res.json({
|
|
2651
|
-
...endCtx,
|
|
2652
|
-
...metadata,
|
|
2653
|
-
__status: "success"
|
|
2654
|
-
});
|
|
2655
|
-
},
|
|
2665
|
+
(endCtx) => resolveDelegation(endCtx, "success"),
|
|
2656
2666
|
"Resolves a delegation request",
|
|
2657
2667
|
{ register: false }
|
|
2658
2668
|
).doOn(`meta.node.graph_completed:${deputyExecId}`).emits(`meta.rest.delegation_resolved:${deputyExecId}`);
|
|
2669
|
+
CadenzaService.createEphemeralMetaTask(
|
|
2670
|
+
"Resolve delegation target lookup failure",
|
|
2671
|
+
(endCtx) => resolveDelegation(endCtx, "error"),
|
|
2672
|
+
"Resolves delegation requests that cannot find a local task or routine",
|
|
2673
|
+
{ register: false }
|
|
2674
|
+
).doOn(targetNotFoundSignal);
|
|
2675
|
+
if (!CadenzaService.get(remoteRoutineName) && !CadenzaService.registry.routines.get(remoteRoutineName)) {
|
|
2676
|
+
CadenzaService.emit(targetNotFoundSignal, {
|
|
2677
|
+
...ctx2,
|
|
2678
|
+
__error: `No task or routine registered for delegation target ${remoteRoutineName}.`,
|
|
2679
|
+
errored: true
|
|
2680
|
+
});
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2659
2683
|
CadenzaService.emit("meta.rest.delegation_requested", {
|
|
2660
2684
|
...ctx2,
|
|
2661
2685
|
__name: ctx2.__remoteRoutineName
|
|
@@ -2826,8 +2850,16 @@ var RestController = class _RestController {
|
|
|
2826
2850
|
CadenzaService.runner.run(routine, context);
|
|
2827
2851
|
return true;
|
|
2828
2852
|
} else {
|
|
2853
|
+
const deputyExecId = context.__metadata?.__deputyExecId ?? context.__deputyExecId;
|
|
2854
|
+
const remoteRoutineName = context.__remoteRoutineName ?? context.__name ?? "unknown";
|
|
2829
2855
|
context.errored = true;
|
|
2830
|
-
context.__error =
|
|
2856
|
+
context.__error = `No task or routine registered for delegation target ${remoteRoutineName}.`;
|
|
2857
|
+
if (deputyExecId) {
|
|
2858
|
+
emit(
|
|
2859
|
+
`meta.rest.delegation_target_not_found:${deputyExecId}`,
|
|
2860
|
+
context
|
|
2861
|
+
);
|
|
2862
|
+
}
|
|
2831
2863
|
emit("meta.runner.failed", context);
|
|
2832
2864
|
return false;
|
|
2833
2865
|
}
|
|
@@ -2850,25 +2882,25 @@ var RestController = class _RestController {
|
|
|
2850
2882
|
(ctx) => {
|
|
2851
2883
|
const { serviceName, serviceAddress, servicePort, protocol } = ctx;
|
|
2852
2884
|
const port = protocol === "https" ? 443 : servicePort;
|
|
2853
|
-
const
|
|
2885
|
+
const URL2 = `${protocol}://${serviceAddress}:${port}`;
|
|
2854
2886
|
const fetchId = `${serviceAddress}_${port}`;
|
|
2855
2887
|
const fetchDiagnostics = this.ensureFetchClientDiagnostics(
|
|
2856
2888
|
fetchId,
|
|
2857
2889
|
serviceName,
|
|
2858
|
-
|
|
2890
|
+
URL2
|
|
2859
2891
|
);
|
|
2860
2892
|
fetchDiagnostics.destroyed = false;
|
|
2861
2893
|
fetchDiagnostics.updatedAt = Date.now();
|
|
2862
|
-
if (CadenzaService.get(`Send Handshake to ${
|
|
2863
|
-
console.error("Fetch client already exists",
|
|
2894
|
+
if (CadenzaService.get(`Send Handshake to ${URL2}`)) {
|
|
2895
|
+
console.error("Fetch client already exists", URL2);
|
|
2864
2896
|
return;
|
|
2865
2897
|
}
|
|
2866
2898
|
const handshakeTask = CadenzaService.createMetaTask(
|
|
2867
|
-
`Send Handshake to ${
|
|
2899
|
+
`Send Handshake to ${URL2}`,
|
|
2868
2900
|
async (ctx2, emit) => {
|
|
2869
2901
|
try {
|
|
2870
2902
|
const response = await this.fetchDataWithTimeout(
|
|
2871
|
-
`${
|
|
2903
|
+
`${URL2}/handshake`,
|
|
2872
2904
|
{
|
|
2873
2905
|
headers: {
|
|
2874
2906
|
"Content-Type": "application/json"
|
|
@@ -2883,10 +2915,10 @@ var RestController = class _RestController {
|
|
|
2883
2915
|
fetchDiagnostics.connected = false;
|
|
2884
2916
|
fetchDiagnostics.lastHandshakeError = error;
|
|
2885
2917
|
fetchDiagnostics.updatedAt = Date.now();
|
|
2886
|
-
this.recordFetchClientError(fetchId, serviceName,
|
|
2918
|
+
this.recordFetchClientError(fetchId, serviceName, URL2, error);
|
|
2887
2919
|
CadenzaService.log(
|
|
2888
2920
|
"Fetch handshake failed.",
|
|
2889
|
-
{ error, serviceName, URL },
|
|
2921
|
+
{ error, serviceName, URL: URL2 },
|
|
2890
2922
|
"warning"
|
|
2891
2923
|
);
|
|
2892
2924
|
emit(`meta.fetch.handshake_failed:${fetchId}`, response);
|
|
@@ -2901,7 +2933,7 @@ var RestController = class _RestController {
|
|
|
2901
2933
|
CadenzaService.log("Fetch client connected.", {
|
|
2902
2934
|
response,
|
|
2903
2935
|
serviceName,
|
|
2904
|
-
URL
|
|
2936
|
+
URL: URL2
|
|
2905
2937
|
});
|
|
2906
2938
|
for (const communicationType of ctx2.communicationTypes) {
|
|
2907
2939
|
emit("global.meta.fetch.service_communication_established", {
|
|
@@ -2916,10 +2948,10 @@ var RestController = class _RestController {
|
|
|
2916
2948
|
fetchDiagnostics.connected = false;
|
|
2917
2949
|
fetchDiagnostics.lastHandshakeError = this.getErrorMessage(e);
|
|
2918
2950
|
fetchDiagnostics.updatedAt = Date.now();
|
|
2919
|
-
this.recordFetchClientError(fetchId, serviceName,
|
|
2951
|
+
this.recordFetchClientError(fetchId, serviceName, URL2, e);
|
|
2920
2952
|
CadenzaService.log(
|
|
2921
2953
|
"Error in fetch handshake",
|
|
2922
|
-
{ error: e, serviceName, URL, ctx: ctx2 },
|
|
2954
|
+
{ error: e, serviceName, URL: URL2, ctx: ctx2 },
|
|
2923
2955
|
"error"
|
|
2924
2956
|
);
|
|
2925
2957
|
return { ...ctx2, __error: e, errored: true };
|
|
@@ -2933,7 +2965,7 @@ var RestController = class _RestController {
|
|
|
2933
2965
|
"global.meta.fetch.service_communication_established"
|
|
2934
2966
|
);
|
|
2935
2967
|
const delegateTask = CadenzaService.createMetaTask(
|
|
2936
|
-
`Delegate flow to REST server ${
|
|
2968
|
+
`Delegate flow to REST server ${URL2}`,
|
|
2937
2969
|
async (ctx2, emit) => {
|
|
2938
2970
|
if (ctx2.__remoteRoutineName === void 0) {
|
|
2939
2971
|
return;
|
|
@@ -2943,7 +2975,7 @@ var RestController = class _RestController {
|
|
|
2943
2975
|
let resultContext;
|
|
2944
2976
|
try {
|
|
2945
2977
|
resultContext = await this.fetchDataWithTimeout(
|
|
2946
|
-
`${
|
|
2978
|
+
`${URL2}/delegation`,
|
|
2947
2979
|
{
|
|
2948
2980
|
headers: {
|
|
2949
2981
|
"Content-Type": "application/json"
|
|
@@ -2959,7 +2991,7 @@ var RestController = class _RestController {
|
|
|
2959
2991
|
this.recordFetchClientError(
|
|
2960
2992
|
fetchId,
|
|
2961
2993
|
serviceName,
|
|
2962
|
-
|
|
2994
|
+
URL2,
|
|
2963
2995
|
resultContext?.__error ?? resultContext?.error ?? "Delegation failed"
|
|
2964
2996
|
);
|
|
2965
2997
|
}
|
|
@@ -2967,7 +2999,7 @@ var RestController = class _RestController {
|
|
|
2967
2999
|
console.error("Error in delegation", e);
|
|
2968
3000
|
fetchDiagnostics.delegationFailures++;
|
|
2969
3001
|
fetchDiagnostics.updatedAt = Date.now();
|
|
2970
|
-
this.recordFetchClientError(fetchId, serviceName,
|
|
3002
|
+
this.recordFetchClientError(fetchId, serviceName, URL2, e);
|
|
2971
3003
|
resultContext = {
|
|
2972
3004
|
__error: `Error: ${e}`,
|
|
2973
3005
|
errored: true,
|
|
@@ -2988,7 +3020,7 @@ var RestController = class _RestController {
|
|
|
2988
3020
|
`meta.service_registry.socket_failed:${fetchId}`
|
|
2989
3021
|
).emitsOnFail("meta.fetch.delegate_failed").attachSignal("meta.fetch.delegated");
|
|
2990
3022
|
const transmitTask = CadenzaService.createMetaTask(
|
|
2991
|
-
`Transmit signal to server ${
|
|
3023
|
+
`Transmit signal to server ${URL2}`,
|
|
2992
3024
|
async (ctx2, emit) => {
|
|
2993
3025
|
if (ctx2.__signalName === void 0) {
|
|
2994
3026
|
return;
|
|
@@ -2998,7 +3030,7 @@ var RestController = class _RestController {
|
|
|
2998
3030
|
let response;
|
|
2999
3031
|
try {
|
|
3000
3032
|
response = await this.fetchDataWithTimeout(
|
|
3001
|
-
`${
|
|
3033
|
+
`${URL2}/signal`,
|
|
3002
3034
|
{
|
|
3003
3035
|
headers: {
|
|
3004
3036
|
"Content-Type": "application/json"
|
|
@@ -3017,7 +3049,7 @@ var RestController = class _RestController {
|
|
|
3017
3049
|
this.recordFetchClientError(
|
|
3018
3050
|
fetchId,
|
|
3019
3051
|
serviceName,
|
|
3020
|
-
|
|
3052
|
+
URL2,
|
|
3021
3053
|
response?.__error ?? response?.error ?? "Signal transmission failed"
|
|
3022
3054
|
);
|
|
3023
3055
|
}
|
|
@@ -3025,7 +3057,7 @@ var RestController = class _RestController {
|
|
|
3025
3057
|
console.error("Error in transmission", e);
|
|
3026
3058
|
fetchDiagnostics.signalFailures++;
|
|
3027
3059
|
fetchDiagnostics.updatedAt = Date.now();
|
|
3028
|
-
this.recordFetchClientError(fetchId, serviceName,
|
|
3060
|
+
this.recordFetchClientError(fetchId, serviceName, URL2, e);
|
|
3029
3061
|
response = {
|
|
3030
3062
|
__error: `Error: ${e}`,
|
|
3031
3063
|
errored: true,
|
|
@@ -3041,14 +3073,14 @@ var RestController = class _RestController {
|
|
|
3041
3073
|
"meta.signal_controller.wildcard_signal_registered"
|
|
3042
3074
|
).emitsOnFail("meta.fetch.signal_transmission_failed").attachSignal("meta.fetch.transmitted");
|
|
3043
3075
|
const statusTask = CadenzaService.createMetaTask(
|
|
3044
|
-
`Request status from ${
|
|
3076
|
+
`Request status from ${URL2}`,
|
|
3045
3077
|
async (ctx2) => {
|
|
3046
3078
|
fetchDiagnostics.statusChecks++;
|
|
3047
3079
|
fetchDiagnostics.updatedAt = Date.now();
|
|
3048
3080
|
let status;
|
|
3049
3081
|
try {
|
|
3050
3082
|
status = await this.fetchDataWithTimeout(
|
|
3051
|
-
`${
|
|
3083
|
+
`${URL2}/status`,
|
|
3052
3084
|
{
|
|
3053
3085
|
method: "GET"
|
|
3054
3086
|
},
|
|
@@ -3060,14 +3092,14 @@ var RestController = class _RestController {
|
|
|
3060
3092
|
this.recordFetchClientError(
|
|
3061
3093
|
fetchId,
|
|
3062
3094
|
serviceName,
|
|
3063
|
-
|
|
3095
|
+
URL2,
|
|
3064
3096
|
status?.__error ?? status?.error ?? "Status check failed"
|
|
3065
3097
|
);
|
|
3066
3098
|
}
|
|
3067
3099
|
} catch (e) {
|
|
3068
3100
|
fetchDiagnostics.statusFailures++;
|
|
3069
3101
|
fetchDiagnostics.updatedAt = Date.now();
|
|
3070
|
-
this.recordFetchClientError(fetchId, serviceName,
|
|
3102
|
+
this.recordFetchClientError(fetchId, serviceName, URL2, e);
|
|
3071
3103
|
status = {
|
|
3072
3104
|
__error: `Error: ${e}`,
|
|
3073
3105
|
errored: true,
|
|
@@ -3082,7 +3114,7 @@ var RestController = class _RestController {
|
|
|
3082
3114
|
fetchDiagnostics.connected = false;
|
|
3083
3115
|
fetchDiagnostics.destroyed = true;
|
|
3084
3116
|
fetchDiagnostics.updatedAt = Date.now();
|
|
3085
|
-
CadenzaService.log("Destroying fetch client", { URL, serviceName });
|
|
3117
|
+
CadenzaService.log("Destroying fetch client", { URL: URL2, serviceName });
|
|
3086
3118
|
handshakeTask.destroy();
|
|
3087
3119
|
delegateTask.destroy();
|
|
3088
3120
|
transmitTask.destroy();
|
|
@@ -3484,13 +3516,13 @@ var SocketController = class _SocketController {
|
|
|
3484
3516
|
Object.assign(base, patch);
|
|
3485
3517
|
base.fetchId = fetchId;
|
|
3486
3518
|
base.updatedAt = now;
|
|
3487
|
-
const
|
|
3488
|
-
if (
|
|
3489
|
-
base.lastError =
|
|
3519
|
+
const errorMessage2 = input.error !== void 0 ? this.getErrorMessage(input.error) : void 0;
|
|
3520
|
+
if (errorMessage2) {
|
|
3521
|
+
base.lastError = errorMessage2;
|
|
3490
3522
|
base.lastErrorAt = now;
|
|
3491
3523
|
base.errorHistory.push({
|
|
3492
3524
|
at: new Date(now).toISOString(),
|
|
3493
|
-
message:
|
|
3525
|
+
message: errorMessage2
|
|
3494
3526
|
});
|
|
3495
3527
|
if (base.errorHistory.length > this.diagnosticsErrorHistoryLimit) {
|
|
3496
3528
|
base.errorHistory.splice(
|
|
@@ -3992,12 +4024,12 @@ var SocketController = class _SocketController {
|
|
|
3992
4024
|
if (ack) ack(response);
|
|
3993
4025
|
resolve(response);
|
|
3994
4026
|
};
|
|
3995
|
-
const resolveWithError = (
|
|
4027
|
+
const resolveWithError = (errorMessage2, fallbackError) => {
|
|
3996
4028
|
settle({
|
|
3997
4029
|
...data,
|
|
3998
4030
|
errored: true,
|
|
3999
|
-
__error:
|
|
4000
|
-
error: fallbackError instanceof Error ? fallbackError.message :
|
|
4031
|
+
__error: errorMessage2,
|
|
4032
|
+
error: fallbackError instanceof Error ? fallbackError.message : errorMessage2,
|
|
4001
4033
|
socketId: runtimeHandle.socket.id,
|
|
4002
4034
|
serviceName,
|
|
4003
4035
|
url
|
|
@@ -4302,19 +4334,19 @@ var SocketController = class _SocketController {
|
|
|
4302
4334
|
url
|
|
4303
4335
|
});
|
|
4304
4336
|
} else {
|
|
4305
|
-
const
|
|
4337
|
+
const errorMessage2 = result?.__error ?? result?.error ?? "Socket handshake failed";
|
|
4306
4338
|
upsertDiagnostics(
|
|
4307
4339
|
{
|
|
4308
4340
|
connected: false,
|
|
4309
4341
|
handshake: false,
|
|
4310
|
-
lastHandshakeError:
|
|
4342
|
+
lastHandshakeError: errorMessage2
|
|
4311
4343
|
},
|
|
4312
|
-
|
|
4344
|
+
errorMessage2
|
|
4313
4345
|
);
|
|
4314
4346
|
applySessionOperation("handshake", {
|
|
4315
4347
|
connected: false,
|
|
4316
4348
|
handshake: false,
|
|
4317
|
-
lastHandshakeError:
|
|
4349
|
+
lastHandshakeError: errorMessage2
|
|
4318
4350
|
});
|
|
4319
4351
|
CadenzaService.log(
|
|
4320
4352
|
"Socket handshake failed",
|
|
@@ -4362,15 +4394,15 @@ var SocketController = class _SocketController {
|
|
|
4362
4394
|
});
|
|
4363
4395
|
}
|
|
4364
4396
|
if (resultContext?.errored || resultContext?.failed) {
|
|
4365
|
-
const
|
|
4397
|
+
const errorMessage2 = resultContext?.__error ?? resultContext?.error ?? "Socket delegation failed";
|
|
4366
4398
|
upsertDiagnostics(
|
|
4367
4399
|
{
|
|
4368
|
-
lastHandshakeError: String(
|
|
4400
|
+
lastHandshakeError: String(errorMessage2)
|
|
4369
4401
|
},
|
|
4370
|
-
|
|
4402
|
+
errorMessage2
|
|
4371
4403
|
);
|
|
4372
4404
|
applySessionOperation("delegate", {
|
|
4373
|
-
lastHandshakeError: String(
|
|
4405
|
+
lastHandshakeError: String(errorMessage2)
|
|
4374
4406
|
});
|
|
4375
4407
|
}
|
|
4376
4408
|
return resultContext;
|
|
@@ -5205,565 +5237,174 @@ var SCHEMA_TYPES = [
|
|
|
5205
5237
|
// src/database/DatabaseController.ts
|
|
5206
5238
|
var import_pg = require("pg");
|
|
5207
5239
|
var import_lodash_es = require("lodash-es");
|
|
5208
|
-
function
|
|
5209
|
-
const
|
|
5210
|
-
|
|
5211
|
-
|
|
5240
|
+
function normalizeIntentToken(value) {
|
|
5241
|
+
const normalized = (0, import_lodash_es.kebabCase)(String(value ?? "").trim());
|
|
5242
|
+
if (!normalized) {
|
|
5243
|
+
throw new Error("Actor token cannot be empty");
|
|
5244
|
+
}
|
|
5245
|
+
return normalized;
|
|
5246
|
+
}
|
|
5247
|
+
function buildPostgresActorName(name) {
|
|
5248
|
+
return `${String(name ?? "").trim()}PostgresActor`;
|
|
5249
|
+
}
|
|
5250
|
+
function validateIntentName(intentName) {
|
|
5251
|
+
if (!intentName || typeof intentName !== "string") {
|
|
5252
|
+
throw new Error("Intent name must be a non-empty string");
|
|
5253
|
+
}
|
|
5254
|
+
if (intentName.length > 100) {
|
|
5255
|
+
throw new Error(`Intent name must be <= 100 characters: ${intentName}`);
|
|
5256
|
+
}
|
|
5257
|
+
if (intentName.includes(" ") || intentName.includes(".") || intentName.includes("\\")) {
|
|
5258
|
+
throw new Error(
|
|
5259
|
+
`Intent name cannot contain spaces, dots or backslashes: ${intentName}`
|
|
5260
|
+
);
|
|
5261
|
+
}
|
|
5262
|
+
}
|
|
5263
|
+
function defaultOperationIntentDescription(operation, tableName) {
|
|
5264
|
+
return `Perform a ${operation} operation on the ${tableName} table`;
|
|
5265
|
+
}
|
|
5266
|
+
function readCustomIntentConfig(customIntent) {
|
|
5267
|
+
if (typeof customIntent === "string") {
|
|
5268
|
+
return {
|
|
5269
|
+
intent: customIntent
|
|
5270
|
+
};
|
|
5271
|
+
}
|
|
5272
|
+
return {
|
|
5273
|
+
intent: customIntent.intent,
|
|
5274
|
+
description: customIntent.description,
|
|
5275
|
+
input: customIntent.input
|
|
5276
|
+
};
|
|
5277
|
+
}
|
|
5278
|
+
function resolveTableOperationIntents(actorName, tableName, table, operation, defaultInputSchema) {
|
|
5279
|
+
const actorToken = normalizeIntentToken(actorName);
|
|
5280
|
+
const defaultIntentName = `${operation}-pg-${actorToken}-${tableName}`;
|
|
5281
|
+
validateIntentName(defaultIntentName);
|
|
5212
5282
|
const intents = [
|
|
5213
5283
|
{
|
|
5214
5284
|
name: defaultIntentName,
|
|
5215
|
-
description:
|
|
5285
|
+
description: defaultOperationIntentDescription(operation, tableName),
|
|
5216
5286
|
input: defaultInputSchema
|
|
5217
5287
|
}
|
|
5218
5288
|
];
|
|
5219
|
-
const
|
|
5220
|
-
const
|
|
5221
|
-
for (const
|
|
5222
|
-
const
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
continue;
|
|
5228
|
-
}
|
|
5229
|
-
if (name.length > 100) {
|
|
5230
|
-
warnings.push(
|
|
5231
|
-
`Skipped custom query intent '${name}' for table '${tableName}': name must be <= 100 characters.`
|
|
5232
|
-
);
|
|
5233
|
-
continue;
|
|
5234
|
-
}
|
|
5235
|
-
if (name.includes(" ") || name.includes(".") || name.includes("\\")) {
|
|
5236
|
-
warnings.push(
|
|
5237
|
-
`Skipped custom query intent '${name}' for table '${tableName}': name cannot contain spaces, dots or backslashes.`
|
|
5289
|
+
const seenNames = /* @__PURE__ */ new Set([defaultIntentName]);
|
|
5290
|
+
const customIntentList = table.customIntents?.[operation] ?? [];
|
|
5291
|
+
for (const rawCustomIntent of customIntentList) {
|
|
5292
|
+
const customIntent = readCustomIntentConfig(rawCustomIntent);
|
|
5293
|
+
const intentName = String(customIntent.intent ?? "").trim();
|
|
5294
|
+
if (!intentName) {
|
|
5295
|
+
throw new Error(
|
|
5296
|
+
`Invalid custom ${operation} intent on table '${tableName}': intent must be a non-empty string`
|
|
5238
5297
|
);
|
|
5239
|
-
continue;
|
|
5240
5298
|
}
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5299
|
+
validateIntentName(intentName);
|
|
5300
|
+
if (seenNames.has(intentName)) {
|
|
5301
|
+
throw new Error(
|
|
5302
|
+
`Duplicate ${operation} intent '${intentName}' on table '${tableName}'`
|
|
5244
5303
|
);
|
|
5245
|
-
continue;
|
|
5246
5304
|
}
|
|
5247
|
-
|
|
5305
|
+
seenNames.add(intentName);
|
|
5248
5306
|
intents.push({
|
|
5249
|
-
name,
|
|
5250
|
-
description:
|
|
5251
|
-
input:
|
|
5307
|
+
name: intentName,
|
|
5308
|
+
description: customIntent.description ?? defaultOperationIntentDescription(operation, tableName),
|
|
5309
|
+
input: customIntent.input ?? defaultInputSchema
|
|
5252
5310
|
});
|
|
5253
5311
|
}
|
|
5254
|
-
return { intents
|
|
5312
|
+
return { intents };
|
|
5313
|
+
}
|
|
5314
|
+
function ensurePlainObject(value, label) {
|
|
5315
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
5316
|
+
throw new Error(`${label} must be an object`);
|
|
5317
|
+
}
|
|
5318
|
+
return value;
|
|
5319
|
+
}
|
|
5320
|
+
function sleep(timeoutMs) {
|
|
5321
|
+
return new Promise((resolve) => {
|
|
5322
|
+
setTimeout(resolve, timeoutMs);
|
|
5323
|
+
});
|
|
5324
|
+
}
|
|
5325
|
+
function normalizePositiveInteger(value, fallback, min = 1) {
|
|
5326
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
5327
|
+
return fallback;
|
|
5328
|
+
}
|
|
5329
|
+
const normalized = Math.trunc(value);
|
|
5330
|
+
if (normalized < min) {
|
|
5331
|
+
return fallback;
|
|
5332
|
+
}
|
|
5333
|
+
return normalized;
|
|
5334
|
+
}
|
|
5335
|
+
function errorMessage(error) {
|
|
5336
|
+
if (error instanceof Error) {
|
|
5337
|
+
return error.message;
|
|
5338
|
+
}
|
|
5339
|
+
return String(error);
|
|
5340
|
+
}
|
|
5341
|
+
function isTransientDatabaseError(error) {
|
|
5342
|
+
if (!error || typeof error !== "object") {
|
|
5343
|
+
return false;
|
|
5344
|
+
}
|
|
5345
|
+
const dbError = error;
|
|
5346
|
+
const code = String(dbError.code ?? "");
|
|
5347
|
+
if (["40001", "40P01", "57P03", "53300", "08006", "08001"].includes(code)) {
|
|
5348
|
+
return true;
|
|
5349
|
+
}
|
|
5350
|
+
const message = String(dbError.message ?? "").toLowerCase();
|
|
5351
|
+
return message.includes("timeout") || message.includes("terminating connection") || message.includes("connection reset");
|
|
5352
|
+
}
|
|
5353
|
+
function isSqlIdentifier(value) {
|
|
5354
|
+
return /^[a-z_][a-z0-9_]*$/.test(value);
|
|
5355
|
+
}
|
|
5356
|
+
function toSafeSqlIdentifier(value, fallback) {
|
|
5357
|
+
const normalized = (0, import_lodash_es.snakeCase)(value || "").replace(/[^a-z0-9_]/g, "_");
|
|
5358
|
+
const candidate = normalized || fallback;
|
|
5359
|
+
if (!isSqlIdentifier(candidate)) {
|
|
5360
|
+
throw new Error(`Invalid SQL identifier: ${value}`);
|
|
5361
|
+
}
|
|
5362
|
+
return candidate;
|
|
5363
|
+
}
|
|
5364
|
+
function isSupportedAggregateFunction(value) {
|
|
5365
|
+
const fn = String(value ?? "").toLowerCase();
|
|
5366
|
+
return ["count", "sum", "avg", "min", "max"].includes(fn);
|
|
5367
|
+
}
|
|
5368
|
+
function buildAggregateAlias(aggregate, index) {
|
|
5369
|
+
const fn = String(aggregate.fn ?? "").toLowerCase();
|
|
5370
|
+
const hasField = typeof aggregate.field === "string" && aggregate.field.trim().length > 0;
|
|
5371
|
+
return toSafeSqlIdentifier(
|
|
5372
|
+
String(aggregate.as ?? `${fn}_${hasField ? aggregate.field : "all"}_${index}`),
|
|
5373
|
+
`${fn || "aggregate"}_${index}`
|
|
5374
|
+
);
|
|
5375
|
+
}
|
|
5376
|
+
function resolveDataRows(data) {
|
|
5377
|
+
if (!data) {
|
|
5378
|
+
return [];
|
|
5379
|
+
}
|
|
5380
|
+
if (Array.isArray(data)) {
|
|
5381
|
+
return data.map((entry) => ensurePlainObject(entry, "data item"));
|
|
5382
|
+
}
|
|
5383
|
+
return [ensurePlainObject(data, "data")];
|
|
5255
5384
|
}
|
|
5256
5385
|
var DatabaseController = class _DatabaseController {
|
|
5257
|
-
/**
|
|
5258
|
-
* Constructor for initializing the `DatabaseService` class.
|
|
5259
|
-
*
|
|
5260
|
-
* This constructor method initializes a sequence of meta tasks to perform the following database-related operations:
|
|
5261
|
-
*
|
|
5262
|
-
* 1. **Database Creation**: Creates a new database with the specified name if it doesn't already exist.
|
|
5263
|
-
* Validates the database name to ensure it conforms to the required format.
|
|
5264
|
-
* 2. **Database Schema Validation**: Validates the structure and constraints of the schema definition provided.
|
|
5265
|
-
* 3. **Table Dependency Management**: Sorts tables within the schema by their dependencies to ensure proper creation order.
|
|
5266
|
-
* 4. **Schema Definition Processing**:
|
|
5267
|
-
* - Converts schema definitions into Data Definition Language (DDL) based on table and field specifications.
|
|
5268
|
-
* - Handles constraints, relationships, and field attributes such as uniqueness, primary keys, nullable fields, etc.
|
|
5269
|
-
* 5. **Index and Primary Key Definition**: Generates SQL for indices and primary keys based on the schema configuration.
|
|
5270
|
-
*
|
|
5271
|
-
* These tasks are encapsulated within a meta routine to provide a structured and procedural approach to database initialization and schema management.
|
|
5272
|
-
*/
|
|
5273
5386
|
constructor() {
|
|
5274
|
-
this.
|
|
5275
|
-
this.
|
|
5387
|
+
this.registrationsByActorName = /* @__PURE__ */ new Map();
|
|
5388
|
+
this.registrationsByActorToken = /* @__PURE__ */ new Map();
|
|
5389
|
+
this.adminDbClient = new import_pg.Pool({
|
|
5276
5390
|
connectionString: process.env.DATABASE_ADDRESS ?? "",
|
|
5277
5391
|
database: "postgres",
|
|
5278
5392
|
ssl: {
|
|
5279
5393
|
rejectUnauthorized: false
|
|
5280
|
-
// ← This bypasses the chain validation error
|
|
5281
5394
|
}
|
|
5282
5395
|
});
|
|
5283
|
-
CadenzaService.
|
|
5284
|
-
"
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
throw new Error(
|
|
5296
|
-
`Invalid database name ${databaseName}. Names must only contain lowercase alphanumeric characters and underscores`
|
|
5297
|
-
);
|
|
5298
|
-
}
|
|
5299
|
-
console.log(`Creating database ${databaseName}`, {
|
|
5300
|
-
connectionString: process.env.DATABASE_ADDRESS ?? "",
|
|
5301
|
-
database: "postgres"
|
|
5302
|
-
});
|
|
5303
|
-
await this.dbClient.query(`CREATE DATABASE ${databaseName}`);
|
|
5304
|
-
console.log(`Database ${databaseName} created`);
|
|
5305
|
-
this.dbClient = new import_pg.Pool({
|
|
5306
|
-
connectionString: process.env.DATABASE_ADDRESS ? process.env.DATABASE_ADDRESS.slice(
|
|
5307
|
-
0,
|
|
5308
|
-
process.env.DATABASE_ADDRESS.lastIndexOf("/")
|
|
5309
|
-
) + "/" + databaseName + "?sslmode=disable" : "",
|
|
5310
|
-
ssl: {
|
|
5311
|
-
rejectUnauthorized: false
|
|
5312
|
-
// ← This bypasses the chain validation error
|
|
5313
|
-
}
|
|
5314
|
-
});
|
|
5315
|
-
this.databaseName = databaseName;
|
|
5316
|
-
return true;
|
|
5317
|
-
} catch (error) {
|
|
5318
|
-
if (error.code === "42P04") {
|
|
5319
|
-
console.log("Database already exists");
|
|
5320
|
-
return true;
|
|
5321
|
-
}
|
|
5322
|
-
console.error("Failed to create database", error);
|
|
5323
|
-
throw new Error(`Failed to create database: ${error.message}`);
|
|
5324
|
-
}
|
|
5325
|
-
},
|
|
5326
|
-
"Creates the target database if it doesn't exist"
|
|
5327
|
-
).then(
|
|
5328
|
-
CadenzaService.createMetaTask(
|
|
5329
|
-
"Validate schema",
|
|
5330
|
-
(ctx) => {
|
|
5331
|
-
const { schema } = ctx;
|
|
5332
|
-
if (!schema?.tables || typeof schema.tables !== "object") {
|
|
5333
|
-
throw new Error("Invalid schema: missing or invalid tables");
|
|
5334
|
-
}
|
|
5335
|
-
for (const [tableName, table] of Object.entries(schema.tables)) {
|
|
5336
|
-
if (!table.fields || typeof table.fields !== "object") {
|
|
5337
|
-
console.log(tableName, "missing fields");
|
|
5338
|
-
throw new Error(`Invalid table ${tableName}: missing fields`);
|
|
5339
|
-
}
|
|
5340
|
-
for (const [fieldName, field] of Object.entries(table.fields)) {
|
|
5341
|
-
if (!fieldName.split("").every((c) => /[a-z_]/.test(c))) {
|
|
5342
|
-
console.log(tableName, "field not lowercase", fieldName);
|
|
5343
|
-
throw new Error(
|
|
5344
|
-
`Invalid field name ${fieldName} for ${tableName}. Field names must only contain lowercase alphanumeric characters and underscores`
|
|
5345
|
-
);
|
|
5346
|
-
}
|
|
5347
|
-
if (!Object.values(SCHEMA_TYPES).includes(field.type)) {
|
|
5348
|
-
console.log(
|
|
5349
|
-
tableName,
|
|
5350
|
-
"field invalid type",
|
|
5351
|
-
fieldName,
|
|
5352
|
-
field.type
|
|
5353
|
-
);
|
|
5354
|
-
throw new Error(
|
|
5355
|
-
`Invalid type ${field.type} for ${tableName}.${fieldName}`
|
|
5356
|
-
);
|
|
5357
|
-
}
|
|
5358
|
-
if (field.references && !field.references.match(/^[\w]+[(\w)]+$/)) {
|
|
5359
|
-
console.log(
|
|
5360
|
-
tableName,
|
|
5361
|
-
"invalid reference",
|
|
5362
|
-
fieldName,
|
|
5363
|
-
field.references
|
|
5364
|
-
);
|
|
5365
|
-
throw new Error(
|
|
5366
|
-
`Invalid reference ${field.references} for ${tableName}.${fieldName}`
|
|
5367
|
-
);
|
|
5368
|
-
}
|
|
5369
|
-
if (table.customSignals) {
|
|
5370
|
-
for (const op of ["query", "insert", "update", "delete"]) {
|
|
5371
|
-
const triggers = table.customSignals.triggers?.[op];
|
|
5372
|
-
const emissions = table.customSignals.emissions?.[op];
|
|
5373
|
-
if (triggers && !Array.isArray(triggers) && typeof triggers !== "object") {
|
|
5374
|
-
console.log(
|
|
5375
|
-
tableName,
|
|
5376
|
-
"invalid triggers",
|
|
5377
|
-
op,
|
|
5378
|
-
triggers
|
|
5379
|
-
);
|
|
5380
|
-
throw new Error(
|
|
5381
|
-
`Invalid triggers for ${tableName}.${op}`
|
|
5382
|
-
);
|
|
5383
|
-
}
|
|
5384
|
-
if (emissions && !Array.isArray(emissions) && typeof emissions !== "object") {
|
|
5385
|
-
console.log(
|
|
5386
|
-
tableName,
|
|
5387
|
-
"invalid emissions",
|
|
5388
|
-
op,
|
|
5389
|
-
emissions
|
|
5390
|
-
);
|
|
5391
|
-
throw new Error(
|
|
5392
|
-
`Invalid emissions for ${tableName}.${op}`
|
|
5393
|
-
);
|
|
5394
|
-
}
|
|
5395
|
-
}
|
|
5396
|
-
}
|
|
5397
|
-
if (table.customIntents?.query) {
|
|
5398
|
-
if (!Array.isArray(table.customIntents.query)) {
|
|
5399
|
-
throw new Error(
|
|
5400
|
-
`Invalid customIntents.query for ${tableName}: expected array`
|
|
5401
|
-
);
|
|
5402
|
-
}
|
|
5403
|
-
for (const customIntent of table.customIntents.query) {
|
|
5404
|
-
if (typeof customIntent !== "string" && (typeof customIntent !== "object" || !customIntent || typeof customIntent.intent !== "string")) {
|
|
5405
|
-
throw new Error(
|
|
5406
|
-
`Invalid custom query intent on ${tableName}: expected string or object with intent`
|
|
5407
|
-
);
|
|
5408
|
-
}
|
|
5409
|
-
}
|
|
5410
|
-
}
|
|
5411
|
-
}
|
|
5412
|
-
}
|
|
5413
|
-
console.log("SCHEMA VALIDATED");
|
|
5414
|
-
return true;
|
|
5415
|
-
},
|
|
5416
|
-
"Validates database schema structure and content"
|
|
5417
|
-
).then(
|
|
5418
|
-
CadenzaService.createMetaTask(
|
|
5419
|
-
"Sort tables by dependencies",
|
|
5420
|
-
this.sortTablesByReferences.bind(this),
|
|
5421
|
-
"Sorts tables by dependencies"
|
|
5422
|
-
).then(
|
|
5423
|
-
CadenzaService.createMetaTask(
|
|
5424
|
-
"Split schema into tables",
|
|
5425
|
-
this.splitTables.bind(this),
|
|
5426
|
-
"Generates DDL for database schema"
|
|
5427
|
-
).then(
|
|
5428
|
-
CadenzaService.createMetaTask(
|
|
5429
|
-
"Generate DDL from table",
|
|
5430
|
-
async (ctx) => {
|
|
5431
|
-
const {
|
|
5432
|
-
ddl,
|
|
5433
|
-
table,
|
|
5434
|
-
tableName,
|
|
5435
|
-
schema,
|
|
5436
|
-
options,
|
|
5437
|
-
sortedTables
|
|
5438
|
-
} = ctx;
|
|
5439
|
-
const fieldDefs = Object.entries(table.fields).map((value) => {
|
|
5440
|
-
const [fieldName, field] = value;
|
|
5441
|
-
let def = `${fieldName} ${field.type.toUpperCase()}`;
|
|
5442
|
-
if (field.type === "varchar")
|
|
5443
|
-
def += `(${field.constraints?.maxLength ?? 255})`;
|
|
5444
|
-
if (field.type === "decimal")
|
|
5445
|
-
def += `(${field.constraints?.precision ?? 10},${field.constraints?.scale ?? 2})`;
|
|
5446
|
-
if (field.primary) def += " PRIMARY KEY";
|
|
5447
|
-
if (field.unique) def += " UNIQUE";
|
|
5448
|
-
if (field.default !== void 0)
|
|
5449
|
-
def += ` DEFAULT ${field.default === "" ? "''" : String(field.default)}`;
|
|
5450
|
-
if (field.required && !field.nullable)
|
|
5451
|
-
def += " NOT NULL";
|
|
5452
|
-
if (field.nullable) def += " NULL";
|
|
5453
|
-
if (field.generated)
|
|
5454
|
-
def += ` GENERATED ALWAYS AS ${field.generated.toUpperCase()} STORED`;
|
|
5455
|
-
if (field.references)
|
|
5456
|
-
def += ` REFERENCES ${field.references} ON DELETE ${field.onDelete || "CASCADE"}`;
|
|
5457
|
-
if (field.encrypted) def += " ENCRYPTED";
|
|
5458
|
-
if (field.constraints?.check) {
|
|
5459
|
-
def += ` CHECK (${field.constraints.check})`;
|
|
5460
|
-
}
|
|
5461
|
-
return def;
|
|
5462
|
-
}).join(", ");
|
|
5463
|
-
if (schema.meta?.dropExisting) {
|
|
5464
|
-
const result = await this.dbClient.query(
|
|
5465
|
-
`DELETE FROM ${tableName};`
|
|
5466
|
-
);
|
|
5467
|
-
console.log("DROP TABLE", tableName, result);
|
|
5468
|
-
}
|
|
5469
|
-
const sql = `CREATE TABLE IF NOT EXISTS ${tableName} (${fieldDefs})`;
|
|
5470
|
-
ddl.push(sql);
|
|
5471
|
-
return {
|
|
5472
|
-
ddl,
|
|
5473
|
-
table,
|
|
5474
|
-
tableName,
|
|
5475
|
-
schema,
|
|
5476
|
-
options,
|
|
5477
|
-
sortedTables
|
|
5478
|
-
};
|
|
5479
|
-
}
|
|
5480
|
-
).then(
|
|
5481
|
-
CadenzaService.createMetaTask("Generate index DDL", (ctx) => {
|
|
5482
|
-
const {
|
|
5483
|
-
ddl,
|
|
5484
|
-
table,
|
|
5485
|
-
tableName,
|
|
5486
|
-
schema,
|
|
5487
|
-
options,
|
|
5488
|
-
sortedTables
|
|
5489
|
-
} = ctx;
|
|
5490
|
-
if (table.indexes) {
|
|
5491
|
-
table.indexes.forEach((fields) => {
|
|
5492
|
-
ddl.push(
|
|
5493
|
-
`CREATE INDEX IF NOT EXISTS idx_${tableName}_${fields.join("_")} ON ${tableName} (${fields.join(", ")});`
|
|
5494
|
-
);
|
|
5495
|
-
});
|
|
5496
|
-
}
|
|
5497
|
-
return {
|
|
5498
|
-
ddl,
|
|
5499
|
-
table,
|
|
5500
|
-
tableName,
|
|
5501
|
-
schema,
|
|
5502
|
-
options,
|
|
5503
|
-
sortedTables
|
|
5504
|
-
};
|
|
5505
|
-
}).then(
|
|
5506
|
-
CadenzaService.createMetaTask(
|
|
5507
|
-
"Generate primary key ddl",
|
|
5508
|
-
(ctx) => {
|
|
5509
|
-
const {
|
|
5510
|
-
ddl,
|
|
5511
|
-
table,
|
|
5512
|
-
tableName,
|
|
5513
|
-
schema,
|
|
5514
|
-
options,
|
|
5515
|
-
sortedTables
|
|
5516
|
-
} = ctx;
|
|
5517
|
-
if (table.primaryKey) {
|
|
5518
|
-
ddl.push(
|
|
5519
|
-
`ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS unique_${tableName}_${table.primaryKey.join("_")};`,
|
|
5520
|
-
// TODO: should be cascade?
|
|
5521
|
-
`ALTER TABLE ${tableName} ADD CONSTRAINT unique_${tableName}_${table.primaryKey.join("_")} PRIMARY KEY (${table.primaryKey.join(", ")});`
|
|
5522
|
-
);
|
|
5523
|
-
}
|
|
5524
|
-
return {
|
|
5525
|
-
ddl,
|
|
5526
|
-
table,
|
|
5527
|
-
tableName,
|
|
5528
|
-
schema,
|
|
5529
|
-
options,
|
|
5530
|
-
sortedTables
|
|
5531
|
-
};
|
|
5532
|
-
}
|
|
5533
|
-
).then(
|
|
5534
|
-
CadenzaService.createMetaTask(
|
|
5535
|
-
"Generate unique index DDL",
|
|
5536
|
-
(ctx) => {
|
|
5537
|
-
const {
|
|
5538
|
-
ddl,
|
|
5539
|
-
table,
|
|
5540
|
-
tableName,
|
|
5541
|
-
schema,
|
|
5542
|
-
options,
|
|
5543
|
-
sortedTables
|
|
5544
|
-
} = ctx;
|
|
5545
|
-
if (table.uniqueConstraints) {
|
|
5546
|
-
table.uniqueConstraints.forEach(
|
|
5547
|
-
(fields) => {
|
|
5548
|
-
ddl.push(
|
|
5549
|
-
`ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS unique_${tableName}_${fields.join("_")};`,
|
|
5550
|
-
// TODO: should be cascade?
|
|
5551
|
-
`ALTER TABLE ${tableName} ADD CONSTRAINT unique_${tableName}_${fields.join("_")} UNIQUE (${fields.join(", ")});`
|
|
5552
|
-
);
|
|
5553
|
-
}
|
|
5554
|
-
);
|
|
5555
|
-
}
|
|
5556
|
-
return {
|
|
5557
|
-
ddl,
|
|
5558
|
-
table,
|
|
5559
|
-
tableName,
|
|
5560
|
-
schema,
|
|
5561
|
-
options,
|
|
5562
|
-
sortedTables
|
|
5563
|
-
};
|
|
5564
|
-
}
|
|
5565
|
-
).then(
|
|
5566
|
-
CadenzaService.createMetaTask(
|
|
5567
|
-
"Generate foreign key DDL",
|
|
5568
|
-
(ctx) => {
|
|
5569
|
-
const {
|
|
5570
|
-
ddl,
|
|
5571
|
-
table,
|
|
5572
|
-
tableName,
|
|
5573
|
-
schema,
|
|
5574
|
-
options,
|
|
5575
|
-
sortedTables
|
|
5576
|
-
} = ctx;
|
|
5577
|
-
if (table.foreignKeys) {
|
|
5578
|
-
for (const foreignKey of table.foreignKeys) {
|
|
5579
|
-
const foreignKeyName = `fk_${tableName}_${foreignKey.fields.join("_")}`;
|
|
5580
|
-
ddl.push(
|
|
5581
|
-
`ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS ${foreignKeyName};`,
|
|
5582
|
-
// TODO: should be cascade?
|
|
5583
|
-
`ALTER TABLE ${tableName} ADD CONSTRAINT ${foreignKeyName} FOREIGN KEY (${foreignKey.fields.join(
|
|
5584
|
-
", "
|
|
5585
|
-
)}) REFERENCES ${foreignKey.tableName} (${foreignKey.referenceFields.join(
|
|
5586
|
-
", "
|
|
5587
|
-
)});`
|
|
5588
|
-
);
|
|
5589
|
-
}
|
|
5590
|
-
}
|
|
5591
|
-
return {
|
|
5592
|
-
ddl,
|
|
5593
|
-
table,
|
|
5594
|
-
tableName,
|
|
5595
|
-
schema,
|
|
5596
|
-
options,
|
|
5597
|
-
sortedTables
|
|
5598
|
-
};
|
|
5599
|
-
}
|
|
5600
|
-
).then(
|
|
5601
|
-
CadenzaService.createMetaTask(
|
|
5602
|
-
"Generate trigger DDL",
|
|
5603
|
-
(ctx) => {
|
|
5604
|
-
const {
|
|
5605
|
-
ddl,
|
|
5606
|
-
table,
|
|
5607
|
-
tableName,
|
|
5608
|
-
schema,
|
|
5609
|
-
options,
|
|
5610
|
-
sortedTables
|
|
5611
|
-
} = ctx;
|
|
5612
|
-
if (table.triggers) {
|
|
5613
|
-
for (const [
|
|
5614
|
-
triggerName,
|
|
5615
|
-
trigger
|
|
5616
|
-
] of Object.entries(table.triggers)) {
|
|
5617
|
-
ddl.push(
|
|
5618
|
-
`CREATE TRIGGER ${triggerName} ${trigger.when} ${trigger.event} ON ${tableName} FOR EACH STATEMENT EXECUTE FUNCTION ${trigger.function};`
|
|
5619
|
-
);
|
|
5620
|
-
}
|
|
5621
|
-
}
|
|
5622
|
-
return {
|
|
5623
|
-
ddl,
|
|
5624
|
-
table,
|
|
5625
|
-
tableName,
|
|
5626
|
-
schema,
|
|
5627
|
-
options,
|
|
5628
|
-
sortedTables
|
|
5629
|
-
};
|
|
5630
|
-
}
|
|
5631
|
-
).then(
|
|
5632
|
-
CadenzaService.createMetaTask(
|
|
5633
|
-
"Generate initial data DDL",
|
|
5634
|
-
(ctx) => {
|
|
5635
|
-
const {
|
|
5636
|
-
ddl,
|
|
5637
|
-
table,
|
|
5638
|
-
tableName,
|
|
5639
|
-
schema,
|
|
5640
|
-
options,
|
|
5641
|
-
sortedTables
|
|
5642
|
-
} = ctx;
|
|
5643
|
-
if (table.initialData) {
|
|
5644
|
-
ddl.push(
|
|
5645
|
-
`INSERT INTO ${tableName} (${table.initialData.fields.join(", ")}) VALUES ${table.initialData.data.map(
|
|
5646
|
-
(row) => `(${row.map(
|
|
5647
|
-
(value) => value === void 0 ? "NULL" : value.charAt(0) === "'" ? value : `'${value}'`
|
|
5648
|
-
).join(", ")})`
|
|
5649
|
-
// TODO: handle non string data
|
|
5650
|
-
).join(", ")} ON CONFLICT DO NOTHING;`
|
|
5651
|
-
);
|
|
5652
|
-
}
|
|
5653
|
-
return {
|
|
5654
|
-
ddl,
|
|
5655
|
-
table,
|
|
5656
|
-
tableName,
|
|
5657
|
-
schema,
|
|
5658
|
-
options,
|
|
5659
|
-
sortedTables
|
|
5660
|
-
};
|
|
5661
|
-
}
|
|
5662
|
-
).then(
|
|
5663
|
-
CadenzaService.createUniqueMetaTask(
|
|
5664
|
-
"Join DDL",
|
|
5665
|
-
(ctx) => {
|
|
5666
|
-
const { joinedContexts } = ctx;
|
|
5667
|
-
const ddl = [];
|
|
5668
|
-
for (const joinedContext of joinedContexts) {
|
|
5669
|
-
ddl.push(...joinedContext.ddl);
|
|
5670
|
-
}
|
|
5671
|
-
ddl.flat();
|
|
5672
|
-
return {
|
|
5673
|
-
ddl,
|
|
5674
|
-
schema: joinedContexts[0].schema,
|
|
5675
|
-
options: joinedContexts[0].options,
|
|
5676
|
-
table: joinedContexts[0].table,
|
|
5677
|
-
tableName: joinedContexts[0].tableName,
|
|
5678
|
-
sortedTables: joinedContexts[0].sortedTables
|
|
5679
|
-
};
|
|
5680
|
-
}
|
|
5681
|
-
).then(
|
|
5682
|
-
CadenzaService.createMetaTask(
|
|
5683
|
-
"Apply Database Changes",
|
|
5684
|
-
async (ctx) => {
|
|
5685
|
-
const { ddl } = ctx;
|
|
5686
|
-
if (ddl && ddl.length > 0) {
|
|
5687
|
-
for (const sql of ddl) {
|
|
5688
|
-
try {
|
|
5689
|
-
await this.dbClient.query(sql);
|
|
5690
|
-
} catch (error) {
|
|
5691
|
-
console.error(
|
|
5692
|
-
"Error applying DDL",
|
|
5693
|
-
sql,
|
|
5694
|
-
error
|
|
5695
|
-
);
|
|
5696
|
-
}
|
|
5697
|
-
}
|
|
5698
|
-
}
|
|
5699
|
-
return true;
|
|
5700
|
-
},
|
|
5701
|
-
"Applies generated DDL to the database"
|
|
5702
|
-
).then(
|
|
5703
|
-
CadenzaService.createMetaTask(
|
|
5704
|
-
"Split schema into tables for task creation",
|
|
5705
|
-
this.splitTables.bind(this),
|
|
5706
|
-
"Splits schema into tables for task creation"
|
|
5707
|
-
).then(
|
|
5708
|
-
CadenzaService.createMetaTask(
|
|
5709
|
-
"Generate tasks",
|
|
5710
|
-
(ctx) => {
|
|
5711
|
-
const { table, tableName, options } = ctx;
|
|
5712
|
-
this.createDatabaseTask(
|
|
5713
|
-
"query",
|
|
5714
|
-
tableName,
|
|
5715
|
-
table,
|
|
5716
|
-
this.queryFunction.bind(this),
|
|
5717
|
-
options
|
|
5718
|
-
);
|
|
5719
|
-
this.createDatabaseTask(
|
|
5720
|
-
"insert",
|
|
5721
|
-
tableName,
|
|
5722
|
-
table,
|
|
5723
|
-
this.insertFunction.bind(this),
|
|
5724
|
-
options
|
|
5725
|
-
);
|
|
5726
|
-
this.createDatabaseTask(
|
|
5727
|
-
"update",
|
|
5728
|
-
tableName,
|
|
5729
|
-
table,
|
|
5730
|
-
this.updateFunction.bind(this),
|
|
5731
|
-
options
|
|
5732
|
-
);
|
|
5733
|
-
this.createDatabaseTask(
|
|
5734
|
-
"delete",
|
|
5735
|
-
tableName,
|
|
5736
|
-
table,
|
|
5737
|
-
this.deleteFunction.bind(this),
|
|
5738
|
-
options
|
|
5739
|
-
);
|
|
5740
|
-
return true;
|
|
5741
|
-
},
|
|
5742
|
-
"Generates auto-tasks for database schema"
|
|
5743
|
-
).then(
|
|
5744
|
-
CadenzaService.createUniqueMetaTask(
|
|
5745
|
-
"Join table tasks",
|
|
5746
|
-
() => {
|
|
5747
|
-
return true;
|
|
5748
|
-
}
|
|
5749
|
-
).emits("meta.database.setup_done")
|
|
5750
|
-
)
|
|
5751
|
-
)
|
|
5752
|
-
)
|
|
5753
|
-
)
|
|
5754
|
-
)
|
|
5755
|
-
)
|
|
5756
|
-
)
|
|
5757
|
-
)
|
|
5758
|
-
)
|
|
5759
|
-
)
|
|
5760
|
-
)
|
|
5761
|
-
)
|
|
5762
|
-
)
|
|
5763
|
-
)
|
|
5764
|
-
)
|
|
5765
|
-
],
|
|
5766
|
-
"Initializes the database service with schema parsing and task/signal generation"
|
|
5396
|
+
CadenzaService.createMetaTask(
|
|
5397
|
+
"Route PostgresActor setup requests",
|
|
5398
|
+
(ctx) => {
|
|
5399
|
+
const registration = this.resolveRegistration(ctx);
|
|
5400
|
+
if (!registration) {
|
|
5401
|
+
return ctx;
|
|
5402
|
+
}
|
|
5403
|
+
this.requestPostgresActorSetup(registration, ctx);
|
|
5404
|
+
return ctx;
|
|
5405
|
+
},
|
|
5406
|
+
"Routes generic database init requests to actor-scoped setup signal.",
|
|
5407
|
+
{ isMeta: true, isSubMeta: true }
|
|
5767
5408
|
).doOn("meta.database_init_requested");
|
|
5768
5409
|
}
|
|
5769
5410
|
static get instance() {
|
|
@@ -5771,632 +5412,1093 @@ var DatabaseController = class _DatabaseController {
|
|
|
5771
5412
|
return this._instance;
|
|
5772
5413
|
}
|
|
5773
5414
|
reset() {
|
|
5774
|
-
this.
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
* The method modifies the client instance by adding timeout tracking and logging functionality to ensure
|
|
5779
|
-
* the client is not held for an extended period and track the last executed query for debugging purposes.
|
|
5780
|
-
*
|
|
5781
|
-
* @return {Promise<PoolClient>} A promise resolving to a database client from the pool with enhanced behavior for query tracking and timeout handling.
|
|
5782
|
-
*/
|
|
5783
|
-
async getClient() {
|
|
5784
|
-
const client = await this.dbClient.connect();
|
|
5785
|
-
const query = client.query;
|
|
5786
|
-
const release = client.release;
|
|
5787
|
-
const timeout = setTimeout(() => {
|
|
5788
|
-
CadenzaService.log(
|
|
5789
|
-
"CRITICAL: A database client has been checked out for more than 5 seconds!",
|
|
5790
|
-
{
|
|
5791
|
-
clientId: client.uuid,
|
|
5792
|
-
query: client.lastQuery,
|
|
5793
|
-
databaseName: this.databaseName
|
|
5794
|
-
},
|
|
5795
|
-
"critical"
|
|
5796
|
-
);
|
|
5797
|
-
}, 5e3);
|
|
5798
|
-
client.query = (...args) => {
|
|
5799
|
-
client.lastQuery = args;
|
|
5800
|
-
return query.apply(client, args);
|
|
5801
|
-
};
|
|
5802
|
-
client.release = () => {
|
|
5803
|
-
clearTimeout(timeout);
|
|
5804
|
-
client.query = query;
|
|
5805
|
-
client.release = release;
|
|
5806
|
-
return release.apply(client);
|
|
5807
|
-
};
|
|
5808
|
-
return client;
|
|
5809
|
-
}
|
|
5810
|
-
async waitForDatabase(transaction, client, context) {
|
|
5811
|
-
for (let i = 0; i < 10; i++) {
|
|
5812
|
-
try {
|
|
5813
|
-
return await transaction(client, context);
|
|
5814
|
-
} catch (err) {
|
|
5815
|
-
if (err && err.message.includes("does not exist")) {
|
|
5816
|
-
CadenzaService.log("Waiting for database to be ready...");
|
|
5817
|
-
await new Promise((res) => setTimeout(res, 1e3));
|
|
5818
|
-
} else {
|
|
5819
|
-
CadenzaService.log(
|
|
5820
|
-
"Database query errored",
|
|
5821
|
-
{ error: err, context },
|
|
5822
|
-
"warning"
|
|
5823
|
-
);
|
|
5824
|
-
return { rows: [] };
|
|
5825
|
-
}
|
|
5415
|
+
for (const registration of this.registrationsByActorName.values()) {
|
|
5416
|
+
const runtimeState = registration.actor.getRuntimeState(registration.actorKey);
|
|
5417
|
+
if (runtimeState?.pool) {
|
|
5418
|
+
runtimeState.pool.end().catch(() => void 0);
|
|
5826
5419
|
}
|
|
5827
5420
|
}
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5421
|
+
this.registrationsByActorName.clear();
|
|
5422
|
+
this.registrationsByActorToken.clear();
|
|
5423
|
+
this.adminDbClient.end().catch(() => void 0);
|
|
5424
|
+
}
|
|
5425
|
+
createPostgresActor(name, schema, description, options) {
|
|
5426
|
+
const actorName = buildPostgresActorName(name);
|
|
5427
|
+
const existing = this.registrationsByActorName.get(actorName);
|
|
5428
|
+
if (existing) {
|
|
5429
|
+
return existing;
|
|
5430
|
+
}
|
|
5431
|
+
const actorToken = normalizeIntentToken(actorName);
|
|
5432
|
+
const actorKey = String(options.databaseName ?? (0, import_lodash_es.snakeCase)(name));
|
|
5433
|
+
const ownerServiceName = options.ownerServiceName ?? CadenzaService.serviceRegistry?.serviceName ?? null;
|
|
5434
|
+
const optionTimeout = typeof options.timeoutMs === "number" ? Number(options.timeoutMs) : Number(options.timeout);
|
|
5435
|
+
const safetyPolicy = {
|
|
5436
|
+
statementTimeoutMs: normalizePositiveInteger(
|
|
5437
|
+
optionTimeout,
|
|
5438
|
+
normalizePositiveInteger(Number(process.env.DATABASE_STATEMENT_TIMEOUT_MS ?? 15e3), 15e3)
|
|
5439
|
+
),
|
|
5440
|
+
retryCount: normalizePositiveInteger(options.retryCount, 3, 0),
|
|
5441
|
+
retryDelayMs: normalizePositiveInteger(Number(process.env.DATABASE_RETRY_DELAY_MS ?? 100), 100),
|
|
5442
|
+
retryDelayMaxMs: normalizePositiveInteger(
|
|
5443
|
+
Number(process.env.DATABASE_RETRY_DELAY_MAX_MS ?? 1e3),
|
|
5444
|
+
1e3
|
|
5445
|
+
),
|
|
5446
|
+
retryDelayFactor: Number(process.env.DATABASE_RETRY_DELAY_FACTOR ?? 2)
|
|
5447
|
+
};
|
|
5448
|
+
const actor = CadenzaService.createActor(
|
|
5449
|
+
{
|
|
5450
|
+
name: actorName,
|
|
5451
|
+
description: description || "Specialized PostgresActor owning pool runtime state and schema-driven DB task generation.",
|
|
5452
|
+
defaultKey: actorKey,
|
|
5453
|
+
keyResolver: (input) => typeof input.databaseName === "string" ? input.databaseName : void 0,
|
|
5454
|
+
loadPolicy: "eager",
|
|
5455
|
+
writeContract: "overwrite",
|
|
5456
|
+
initState: {
|
|
5457
|
+
actorName,
|
|
5458
|
+
actorToken,
|
|
5459
|
+
ownerServiceName,
|
|
5460
|
+
databaseName: actorKey,
|
|
5461
|
+
status: "idle",
|
|
5462
|
+
schemaVersion: Number(schema.version ?? 1),
|
|
5463
|
+
setupStartedAt: null,
|
|
5464
|
+
setupCompletedAt: null,
|
|
5465
|
+
lastHealthCheckAt: null,
|
|
5466
|
+
lastError: null,
|
|
5467
|
+
tables: Object.keys(schema.tables ?? {}),
|
|
5468
|
+
safetyPolicy
|
|
5867
5469
|
}
|
|
5868
|
-
}
|
|
5470
|
+
},
|
|
5471
|
+
{ isMeta: Boolean(options.isMeta) }
|
|
5472
|
+
);
|
|
5473
|
+
const registration = {
|
|
5474
|
+
ownerServiceName,
|
|
5475
|
+
databaseName: actorKey,
|
|
5476
|
+
actorName,
|
|
5477
|
+
actorToken,
|
|
5478
|
+
actorKey,
|
|
5479
|
+
setupSignal: `meta.postgres_actor.setup_requested.${actorToken}`,
|
|
5480
|
+
setupDoneSignal: `meta.postgres_actor.setup_done.${actorToken}`,
|
|
5481
|
+
setupFailedSignal: `meta.postgres_actor.setup_failed.${actorToken}`,
|
|
5482
|
+
actor,
|
|
5483
|
+
schema,
|
|
5484
|
+
description,
|
|
5485
|
+
options,
|
|
5486
|
+
tasksGenerated: false,
|
|
5487
|
+
intentNames: /* @__PURE__ */ new Set()
|
|
5488
|
+
};
|
|
5489
|
+
this.registrationsByActorName.set(actorName, registration);
|
|
5490
|
+
this.registrationsByActorToken.set(actorToken, registration);
|
|
5491
|
+
this.registerSetupTask(registration);
|
|
5492
|
+
return registration;
|
|
5493
|
+
}
|
|
5494
|
+
requestPostgresActorSetup(registrationOrName, ctx = {}) {
|
|
5495
|
+
const registration = typeof registrationOrName === "string" ? this.resolveRegistration({
|
|
5496
|
+
actorName: registrationOrName
|
|
5497
|
+
}) : registrationOrName;
|
|
5498
|
+
if (!registration) {
|
|
5499
|
+
return void 0;
|
|
5869
5500
|
}
|
|
5870
|
-
const
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
visit(dep);
|
|
5501
|
+
const payload = {
|
|
5502
|
+
...ctx,
|
|
5503
|
+
actorName: registration.actorName,
|
|
5504
|
+
actorToken: registration.actorToken,
|
|
5505
|
+
databaseName: registration.databaseName,
|
|
5506
|
+
ownerServiceName: registration.ownerServiceName,
|
|
5507
|
+
options: {
|
|
5508
|
+
...ctx.options ?? {},
|
|
5509
|
+
actorName: registration.actorName,
|
|
5510
|
+
actorToken: registration.actorToken,
|
|
5511
|
+
ownerServiceName: registration.ownerServiceName,
|
|
5512
|
+
databaseName: registration.databaseName
|
|
5883
5513
|
}
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5514
|
+
};
|
|
5515
|
+
const runtimeState = registration.actor.getRuntimeState(registration.actorKey);
|
|
5516
|
+
if (runtimeState?.ready) {
|
|
5517
|
+
this.emitSetupDone(registration, payload);
|
|
5518
|
+
return registration;
|
|
5887
5519
|
}
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5520
|
+
CadenzaService.emit(registration.setupSignal, payload);
|
|
5521
|
+
return registration;
|
|
5522
|
+
}
|
|
5523
|
+
resolveRegistration(ctx) {
|
|
5524
|
+
const rawActorToken = String(
|
|
5525
|
+
ctx.options?.actorToken ?? ctx.actorToken ?? ""
|
|
5526
|
+
).trim();
|
|
5527
|
+
if (rawActorToken) {
|
|
5528
|
+
const actorToken = normalizeIntentToken(rawActorToken);
|
|
5529
|
+
const registration = this.registrationsByActorToken.get(actorToken);
|
|
5530
|
+
if (registration) {
|
|
5531
|
+
return registration;
|
|
5891
5532
|
}
|
|
5892
5533
|
}
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5534
|
+
const rawActorName = String(
|
|
5535
|
+
ctx.options?.actorName ?? ctx.actorName ?? ctx.options?.postgresActorName ?? ctx.postgresActorName ?? ""
|
|
5536
|
+
).trim();
|
|
5537
|
+
if (rawActorName) {
|
|
5538
|
+
const registration = this.registrationsByActorName.get(rawActorName) ?? this.registrationsByActorName.get(buildPostgresActorName(rawActorName));
|
|
5539
|
+
if (registration) {
|
|
5540
|
+
return registration;
|
|
5896
5541
|
}
|
|
5897
5542
|
}
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
* @param {string[]} ctx.sortedTables - An array of table names sorted in a specific order.
|
|
5906
|
-
* @param {Object} ctx.schema - The schema object that includes table definitions.
|
|
5907
|
-
* @param {Object} [ctx.options={}] - Optional configuration options for processing tables.
|
|
5908
|
-
*
|
|
5909
|
-
* @return {AsyncGenerator} An asynchronous generator that yields objects containing the table definition, metadata, and other context details.
|
|
5910
|
-
*/
|
|
5911
|
-
async *splitTables(ctx) {
|
|
5912
|
-
const { sortedTables, schema, options = {} } = ctx;
|
|
5913
|
-
for (const tableName of sortedTables) {
|
|
5914
|
-
const table = schema.tables[tableName];
|
|
5915
|
-
yield { ddl: [], table, tableName, schema, options, sortedTables };
|
|
5543
|
+
const legacyServiceName = String(
|
|
5544
|
+
ctx.options?.serviceName ?? ctx.serviceName ?? ""
|
|
5545
|
+
).trim();
|
|
5546
|
+
if (legacyServiceName) {
|
|
5547
|
+
return this.registrationsByActorName.get(
|
|
5548
|
+
buildPostgresActorName(legacyServiceName)
|
|
5549
|
+
);
|
|
5916
5550
|
}
|
|
5551
|
+
return void 0;
|
|
5917
5552
|
}
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
return camelCasedRow;
|
|
5931
|
-
});
|
|
5553
|
+
emitSetupDone(registration, payload) {
|
|
5554
|
+
const resolvedPayload = {
|
|
5555
|
+
...payload,
|
|
5556
|
+
actorName: registration.actorName,
|
|
5557
|
+
actorToken: registration.actorToken,
|
|
5558
|
+
databaseName: registration.databaseName,
|
|
5559
|
+
ownerServiceName: registration.ownerServiceName,
|
|
5560
|
+
__success: true
|
|
5561
|
+
};
|
|
5562
|
+
CadenzaService.emit(registration.setupDoneSignal, resolvedPayload);
|
|
5563
|
+
CadenzaService.emit("meta.postgres_actor.setup_done", resolvedPayload);
|
|
5564
|
+
CadenzaService.emit("meta.database.setup_done", resolvedPayload);
|
|
5932
5565
|
}
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5566
|
+
registerSetupTask(registration) {
|
|
5567
|
+
CadenzaService.createMetaTask(
|
|
5568
|
+
`Setup ${registration.actorName}`,
|
|
5569
|
+
registration.actor.task(
|
|
5570
|
+
async ({ input, state, runtimeState, setState, setRuntimeState, emit }) => {
|
|
5571
|
+
const requestedDatabaseName = String(
|
|
5572
|
+
input.options?.databaseName ?? input.databaseName ?? registration.databaseName
|
|
5573
|
+
);
|
|
5574
|
+
if (requestedDatabaseName !== registration.databaseName) {
|
|
5575
|
+
return input;
|
|
5576
|
+
}
|
|
5577
|
+
setState({
|
|
5578
|
+
...state,
|
|
5579
|
+
status: "initializing",
|
|
5580
|
+
setupStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5581
|
+
lastError: null
|
|
5582
|
+
});
|
|
5583
|
+
const priorRuntimePool = runtimeState?.pool ?? null;
|
|
5584
|
+
if (priorRuntimePool) {
|
|
5585
|
+
await priorRuntimePool.end().catch(() => void 0);
|
|
5586
|
+
}
|
|
5587
|
+
try {
|
|
5588
|
+
await this.createDatabaseIfMissing(requestedDatabaseName);
|
|
5589
|
+
const pool = this.createTargetPool(
|
|
5590
|
+
requestedDatabaseName,
|
|
5591
|
+
state.safetyPolicy.statementTimeoutMs
|
|
5592
|
+
);
|
|
5593
|
+
await this.checkPoolHealth(pool, state.safetyPolicy);
|
|
5594
|
+
this.validateSchema({
|
|
5595
|
+
schema: registration.schema,
|
|
5596
|
+
options: registration.options
|
|
5597
|
+
});
|
|
5598
|
+
const sortedTables = this.sortTablesByReferences({
|
|
5599
|
+
schema: registration.schema
|
|
5600
|
+
}).sortedTables;
|
|
5601
|
+
const ddlStatements = this.buildSchemaDdlStatements(
|
|
5602
|
+
registration.schema,
|
|
5603
|
+
sortedTables
|
|
5604
|
+
);
|
|
5605
|
+
await this.applyDdlStatements(pool, ddlStatements);
|
|
5606
|
+
if (!registration.tasksGenerated) {
|
|
5607
|
+
this.generateDatabaseTasks(registration);
|
|
5608
|
+
registration.tasksGenerated = true;
|
|
5609
|
+
}
|
|
5610
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
5611
|
+
setRuntimeState({
|
|
5612
|
+
pool,
|
|
5613
|
+
ready: true,
|
|
5614
|
+
pendingQueries: 0,
|
|
5615
|
+
lastHealthCheckAt: Date.now(),
|
|
5616
|
+
lastError: null
|
|
5617
|
+
});
|
|
5618
|
+
setState({
|
|
5619
|
+
...state,
|
|
5620
|
+
status: "ready",
|
|
5621
|
+
setupCompletedAt: nowIso,
|
|
5622
|
+
lastHealthCheckAt: nowIso,
|
|
5623
|
+
lastError: null,
|
|
5624
|
+
tables: Object.keys(registration.schema.tables ?? {})
|
|
5625
|
+
});
|
|
5626
|
+
this.emitSetupDone(registration, {
|
|
5627
|
+
...input
|
|
5628
|
+
});
|
|
5629
|
+
return {
|
|
5630
|
+
...input,
|
|
5631
|
+
__success: true,
|
|
5632
|
+
actorName: registration.actorName,
|
|
5633
|
+
databaseName: registration.databaseName,
|
|
5634
|
+
ownerServiceName: registration.ownerServiceName
|
|
5635
|
+
};
|
|
5636
|
+
} catch (error) {
|
|
5637
|
+
const message = errorMessage(error);
|
|
5638
|
+
setRuntimeState({
|
|
5639
|
+
pool: null,
|
|
5640
|
+
ready: false,
|
|
5641
|
+
pendingQueries: 0,
|
|
5642
|
+
lastHealthCheckAt: runtimeState?.lastHealthCheckAt ?? null,
|
|
5643
|
+
lastError: message
|
|
5644
|
+
});
|
|
5645
|
+
setState({
|
|
5646
|
+
...state,
|
|
5647
|
+
status: "error",
|
|
5648
|
+
setupCompletedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5649
|
+
lastError: message
|
|
5650
|
+
});
|
|
5651
|
+
throw error;
|
|
5652
|
+
}
|
|
5653
|
+
},
|
|
5654
|
+
{ mode: "write" }
|
|
5655
|
+
),
|
|
5656
|
+
"Initializes PostgresActor runtime pool, applies schema, and generates CRUD tasks/intents.",
|
|
5657
|
+
{ isMeta: true }
|
|
5658
|
+
).doOn(registration.setupSignal).emitsOnFail(
|
|
5659
|
+
registration.setupFailedSignal,
|
|
5660
|
+
"meta.postgres_actor.setup_failed",
|
|
5661
|
+
"meta.database.setup_failed"
|
|
5662
|
+
);
|
|
5663
|
+
}
|
|
5664
|
+
createTargetPool(databaseName, statementTimeoutMs) {
|
|
5665
|
+
const connectionString = this.buildDatabaseConnectionString(databaseName);
|
|
5666
|
+
return new import_pg.Pool({
|
|
5667
|
+
connectionString,
|
|
5668
|
+
statement_timeout: statementTimeoutMs,
|
|
5669
|
+
query_timeout: statementTimeoutMs,
|
|
5670
|
+
ssl: {
|
|
5671
|
+
rejectUnauthorized: false
|
|
5672
|
+
}
|
|
5673
|
+
});
|
|
5674
|
+
}
|
|
5675
|
+
buildDatabaseConnectionString(databaseName) {
|
|
5676
|
+
const base = process.env.DATABASE_ADDRESS ?? "";
|
|
5677
|
+
if (!base) {
|
|
5678
|
+
throw new Error("DATABASE_ADDRESS environment variable is required");
|
|
5679
|
+
}
|
|
5680
|
+
try {
|
|
5681
|
+
const parsed = new URL(base);
|
|
5682
|
+
parsed.pathname = `/${databaseName}`;
|
|
5683
|
+
if (!parsed.searchParams.has("sslmode")) {
|
|
5684
|
+
parsed.searchParams.set("sslmode", "disable");
|
|
5685
|
+
}
|
|
5686
|
+
return parsed.toString();
|
|
5687
|
+
} catch {
|
|
5688
|
+
const lastSlashIndex = base.lastIndexOf("/");
|
|
5689
|
+
if (lastSlashIndex === -1) {
|
|
5690
|
+
throw new Error("DATABASE_ADDRESS must be a valid postgres connection string");
|
|
5691
|
+
}
|
|
5692
|
+
const root = base.slice(0, lastSlashIndex + 1);
|
|
5693
|
+
return `${root}${databaseName}?sslmode=disable`;
|
|
5694
|
+
}
|
|
5695
|
+
}
|
|
5696
|
+
async createDatabaseIfMissing(databaseName) {
|
|
5697
|
+
if (!isSqlIdentifier(databaseName)) {
|
|
5698
|
+
throw new Error(
|
|
5699
|
+
`Invalid database name '${databaseName}'. Names must contain only lowercase alphanumerics and underscores and cannot start with a number.`
|
|
5700
|
+
);
|
|
5701
|
+
}
|
|
5702
|
+
try {
|
|
5703
|
+
await this.adminDbClient.query(`CREATE DATABASE ${databaseName}`);
|
|
5704
|
+
CadenzaService.log(`Database ${databaseName} created.`);
|
|
5705
|
+
} catch (error) {
|
|
5706
|
+
if (error?.code === "42P04") {
|
|
5707
|
+
CadenzaService.log(`Database ${databaseName} already exists.`);
|
|
5708
|
+
return;
|
|
5709
|
+
}
|
|
5710
|
+
throw new Error(`Failed to create database '${databaseName}': ${errorMessage(error)}`);
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5713
|
+
async checkPoolHealth(pool, safetyPolicy) {
|
|
5714
|
+
await this.runWithRetries(
|
|
5715
|
+
async () => {
|
|
5716
|
+
await this.withTimeout(
|
|
5717
|
+
() => pool.query("SELECT 1 as health"),
|
|
5718
|
+
safetyPolicy.statementTimeoutMs,
|
|
5719
|
+
"Database health check timed out"
|
|
5720
|
+
);
|
|
5721
|
+
},
|
|
5722
|
+
safetyPolicy,
|
|
5723
|
+
"Health check"
|
|
5724
|
+
);
|
|
5725
|
+
}
|
|
5726
|
+
getPoolOrThrow(registration) {
|
|
5727
|
+
const runtimeState = registration.actor.getRuntimeState(
|
|
5728
|
+
registration.actorKey
|
|
5729
|
+
);
|
|
5730
|
+
if (!runtimeState || !runtimeState.ready || !runtimeState.pool) {
|
|
5731
|
+
throw new Error(
|
|
5732
|
+
`PostgresActor '${registration.actorName}' is not ready. Ensure setup completed before running DB tasks.`
|
|
5733
|
+
);
|
|
5734
|
+
}
|
|
5735
|
+
return runtimeState.pool;
|
|
5736
|
+
}
|
|
5737
|
+
async withTimeout(work, timeoutMs, timeoutMessage) {
|
|
5738
|
+
let timeoutHandle = null;
|
|
5739
|
+
try {
|
|
5740
|
+
return await Promise.race([
|
|
5741
|
+
work(),
|
|
5742
|
+
new Promise((_, reject) => {
|
|
5743
|
+
timeoutHandle = setTimeout(() => {
|
|
5744
|
+
reject(new Error(timeoutMessage));
|
|
5745
|
+
}, timeoutMs);
|
|
5746
|
+
})
|
|
5747
|
+
]);
|
|
5748
|
+
} finally {
|
|
5749
|
+
if (timeoutHandle) {
|
|
5750
|
+
clearTimeout(timeoutHandle);
|
|
5751
|
+
}
|
|
5752
|
+
}
|
|
5753
|
+
}
|
|
5754
|
+
async runWithRetries(work, safetyPolicy, operationLabel) {
|
|
5755
|
+
const attempts = normalizePositiveInteger(safetyPolicy.retryCount, 0, 0) + 1;
|
|
5756
|
+
let delayMs = normalizePositiveInteger(safetyPolicy.retryDelayMs, 100);
|
|
5757
|
+
const maxDelayMs = normalizePositiveInteger(safetyPolicy.retryDelayMaxMs, 1e3);
|
|
5758
|
+
const factor = Number.isFinite(safetyPolicy.retryDelayFactor) ? Math.max(1, safetyPolicy.retryDelayFactor) : 1;
|
|
5759
|
+
let lastError;
|
|
5760
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
5761
|
+
try {
|
|
5762
|
+
return await work();
|
|
5763
|
+
} catch (error) {
|
|
5764
|
+
lastError = error;
|
|
5765
|
+
const transient = isTransientDatabaseError(error);
|
|
5766
|
+
if (!transient || attempt >= attempts) {
|
|
5767
|
+
break;
|
|
5768
|
+
}
|
|
5769
|
+
CadenzaService.log(
|
|
5770
|
+
`${operationLabel} failed with transient error. Retrying...`,
|
|
5771
|
+
{
|
|
5772
|
+
attempt,
|
|
5773
|
+
attempts,
|
|
5774
|
+
delayMs,
|
|
5775
|
+
error: errorMessage(error)
|
|
5776
|
+
},
|
|
5777
|
+
"warning"
|
|
5778
|
+
);
|
|
5779
|
+
await sleep(delayMs);
|
|
5780
|
+
delayMs = Math.min(Math.trunc(delayMs * factor), maxDelayMs);
|
|
5781
|
+
}
|
|
5782
|
+
}
|
|
5783
|
+
throw lastError;
|
|
5784
|
+
}
|
|
5785
|
+
async executeWithTransaction(pool, transaction, callback) {
|
|
5786
|
+
if (!transaction) {
|
|
5787
|
+
return callback(pool);
|
|
5788
|
+
}
|
|
5789
|
+
const client = await pool.connect();
|
|
5790
|
+
try {
|
|
5791
|
+
await client.query("BEGIN");
|
|
5792
|
+
const result = await callback(client);
|
|
5793
|
+
await client.query("COMMIT");
|
|
5794
|
+
return result;
|
|
5795
|
+
} catch (error) {
|
|
5796
|
+
await client.query("ROLLBACK");
|
|
5797
|
+
throw error;
|
|
5798
|
+
} finally {
|
|
5799
|
+
client.release();
|
|
5800
|
+
}
|
|
5801
|
+
}
|
|
5802
|
+
async queryFunction(registration, tableName, context) {
|
|
5803
|
+
const {
|
|
5804
|
+
filter = {},
|
|
5805
|
+
fields = [],
|
|
5806
|
+
joins = {},
|
|
5807
|
+
sort = {},
|
|
5808
|
+
limit,
|
|
5809
|
+
offset,
|
|
5810
|
+
queryMode = "rows",
|
|
5811
|
+
aggregates = [],
|
|
5812
|
+
groupBy = []
|
|
5948
5813
|
} = context;
|
|
5949
|
-
|
|
5814
|
+
const pool = this.getPoolOrThrow(registration);
|
|
5815
|
+
const statementTimeoutMs = this.resolveSafetyPolicy(registration).statementTimeoutMs;
|
|
5816
|
+
const resolvedMode = queryMode;
|
|
5817
|
+
const aggregateDefinitions = Array.isArray(aggregates) ? aggregates : [];
|
|
5818
|
+
const groupByFields = Array.isArray(groupBy) ? groupBy : [];
|
|
5950
5819
|
const params = [];
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
if (
|
|
5955
|
-
sql
|
|
5820
|
+
const whereClause = Object.keys(filter).length > 0 ? this.buildWhereClause(filter, params) : "";
|
|
5821
|
+
const joinClause = Object.keys(joins).length > 0 ? this.buildJoinClause(joins) : "";
|
|
5822
|
+
let sql;
|
|
5823
|
+
if (resolvedMode === "count") {
|
|
5824
|
+
sql = `SELECT COUNT(*)::bigint AS count FROM ${tableName} ${joinClause} ${whereClause}`;
|
|
5825
|
+
} else if (resolvedMode === "exists") {
|
|
5826
|
+
sql = `SELECT EXISTS(SELECT 1 FROM ${tableName} ${joinClause} ${whereClause}) AS exists`;
|
|
5827
|
+
} else if (resolvedMode === "aggregate") {
|
|
5828
|
+
if (aggregateDefinitions.length === 0) {
|
|
5829
|
+
throw new Error("Aggregate queries require at least one aggregate definition");
|
|
5830
|
+
}
|
|
5831
|
+
const aggregateExpressions = aggregateDefinitions.map(
|
|
5832
|
+
(aggregate, index) => {
|
|
5833
|
+
const fn = String(aggregate.fn ?? "").toLowerCase();
|
|
5834
|
+
if (!isSupportedAggregateFunction(fn)) {
|
|
5835
|
+
throw new Error(`Unsupported aggregate function '${aggregate.fn}'`);
|
|
5836
|
+
}
|
|
5837
|
+
const hasField = typeof aggregate.field === "string" && aggregate.field.trim().length > 0;
|
|
5838
|
+
if (fn !== "count" && !hasField) {
|
|
5839
|
+
throw new Error(`Aggregate '${fn}' requires a field`);
|
|
5840
|
+
}
|
|
5841
|
+
const fieldExpression = hasField ? (0, import_lodash_es.snakeCase)(String(aggregate.field)) : "*";
|
|
5842
|
+
const distinctPrefix = aggregate.distinct ? "DISTINCT " : "";
|
|
5843
|
+
const expression = fn === "count" && !hasField ? "COUNT(*)" : `${fn.toUpperCase()}(${distinctPrefix}${fieldExpression})`;
|
|
5844
|
+
const alias = buildAggregateAlias(aggregate, index);
|
|
5845
|
+
return `${expression} AS ${alias}`;
|
|
5846
|
+
}
|
|
5847
|
+
);
|
|
5848
|
+
const groupByExpressions = groupByFields.map((field) => (0, import_lodash_es.snakeCase)(field));
|
|
5849
|
+
const selectExpressions = [...groupByExpressions, ...aggregateExpressions];
|
|
5850
|
+
sql = `SELECT ${selectExpressions.join(", ")} FROM ${tableName} ${joinClause} ${whereClause}`;
|
|
5851
|
+
if (groupByExpressions.length > 0) {
|
|
5852
|
+
sql += ` GROUP BY ${groupByExpressions.join(", ")}`;
|
|
5853
|
+
}
|
|
5854
|
+
} else {
|
|
5855
|
+
sql = `SELECT ${fields.length ? fields.map(import_lodash_es.snakeCase).join(", ") : "*"} FROM ${tableName} ${joinClause} ${whereClause}`;
|
|
5956
5856
|
}
|
|
5957
|
-
if (Object.keys(sort).length > 0) {
|
|
5958
|
-
sql += " ORDER BY " + Object.entries(sort).map(([field, direction]) => `${field} ${direction}`).join(", ");
|
|
5857
|
+
if (Object.keys(sort).length > 0 && resolvedMode !== "count" && resolvedMode !== "exists") {
|
|
5858
|
+
sql += " ORDER BY " + Object.entries(sort).map(([field, direction]) => `${(0, import_lodash_es.snakeCase)(field)} ${direction}`).join(", ");
|
|
5959
5859
|
}
|
|
5960
|
-
if (
|
|
5860
|
+
if (resolvedMode === "one") {
|
|
5861
|
+
sql += ` LIMIT $${params.length + 1}`;
|
|
5862
|
+
params.push(1);
|
|
5863
|
+
} else if (resolvedMode !== "count" && resolvedMode !== "exists" && limit !== void 0) {
|
|
5961
5864
|
sql += ` LIMIT $${params.length + 1}`;
|
|
5962
5865
|
params.push(limit);
|
|
5963
5866
|
}
|
|
5964
|
-
if (offset !== void 0) {
|
|
5867
|
+
if (resolvedMode !== "count" && resolvedMode !== "exists" && offset !== void 0) {
|
|
5965
5868
|
sql += ` OFFSET $${params.length + 1}`;
|
|
5966
5869
|
params.push(offset);
|
|
5967
5870
|
}
|
|
5968
5871
|
try {
|
|
5969
|
-
const result = await this.
|
|
5872
|
+
const result = await this.withTimeout(
|
|
5873
|
+
() => pool.query(sql, params),
|
|
5874
|
+
statementTimeoutMs,
|
|
5875
|
+
`Query timeout on table ${tableName}`
|
|
5876
|
+
);
|
|
5970
5877
|
const rows = this.toCamelCase(result.rows);
|
|
5878
|
+
const rowCount = Number(result.rowCount ?? 0);
|
|
5879
|
+
if (resolvedMode === "count") {
|
|
5880
|
+
return {
|
|
5881
|
+
count: Number(rows[0]?.count ?? 0),
|
|
5882
|
+
rowCount: Number(rows[0]?.count ?? 0),
|
|
5883
|
+
__success: true
|
|
5884
|
+
};
|
|
5885
|
+
}
|
|
5886
|
+
if (resolvedMode === "exists") {
|
|
5887
|
+
const exists = Boolean(rows[0]?.exists);
|
|
5888
|
+
return {
|
|
5889
|
+
exists,
|
|
5890
|
+
rowCount: exists ? 1 : 0,
|
|
5891
|
+
__success: true
|
|
5892
|
+
};
|
|
5893
|
+
}
|
|
5894
|
+
if (resolvedMode === "one") {
|
|
5895
|
+
return {
|
|
5896
|
+
[`${(0, import_lodash_es.camelCase)(tableName)}`]: rows[0] ?? null,
|
|
5897
|
+
rowCount,
|
|
5898
|
+
__success: true
|
|
5899
|
+
};
|
|
5900
|
+
}
|
|
5901
|
+
if (resolvedMode === "aggregate") {
|
|
5902
|
+
return {
|
|
5903
|
+
aggregates: rows,
|
|
5904
|
+
rowCount,
|
|
5905
|
+
__success: true
|
|
5906
|
+
};
|
|
5907
|
+
}
|
|
5971
5908
|
return {
|
|
5972
5909
|
[`${(0, import_lodash_es.camelCase)(tableName)}s`]: rows,
|
|
5973
|
-
rowCount
|
|
5974
|
-
__success: true
|
|
5975
|
-
...context
|
|
5910
|
+
rowCount,
|
|
5911
|
+
__success: true
|
|
5976
5912
|
};
|
|
5977
5913
|
} catch (error) {
|
|
5978
5914
|
return {
|
|
5979
|
-
|
|
5915
|
+
rowCount: 0,
|
|
5980
5916
|
errored: true,
|
|
5981
|
-
__error: `Query failed: ${error
|
|
5917
|
+
__error: `Query failed: ${errorMessage(error)}`,
|
|
5982
5918
|
__success: false
|
|
5983
5919
|
};
|
|
5984
5920
|
}
|
|
5985
5921
|
}
|
|
5986
|
-
|
|
5987
|
-
* Inserts data into the specified database table with optional conflict handling.
|
|
5988
|
-
*
|
|
5989
|
-
* @param {string} tableName - The name of the target database table.
|
|
5990
|
-
* @param {DbOperationPayload} context - The context containing data to insert, transaction settings, field mappings, conflict resolution options, and other configurations.
|
|
5991
|
-
* - `data` (object | array): The data to be inserted into the database.
|
|
5992
|
-
* - `transaction` (boolean): Specifies whether the operation should use a transaction. Defaults to true.
|
|
5993
|
-
* - `fields` (array): The fields to return in the result after insertion.
|
|
5994
|
-
* - `onConflict` (object): Options for handling conflicts on insert.
|
|
5995
|
-
* - `target` (array): Columns to determine conflicts.
|
|
5996
|
-
* - `action` (object): Specifies the action to take on conflict, such as updating specified fields.
|
|
5997
|
-
* - `awaitExists` (object): Specifies foreign key references to wait for to ensure existence before insertion.
|
|
5998
|
-
*
|
|
5999
|
-
* @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.
|
|
6000
|
-
*/
|
|
6001
|
-
async insertFunction(tableName, context) {
|
|
5922
|
+
async insertFunction(registration, tableName, context) {
|
|
6002
5923
|
const { data, transaction = true, fields = [], onConflict } = context;
|
|
6003
5924
|
if (!data || Array.isArray(data) && data.length === 0) {
|
|
6004
|
-
return {
|
|
5925
|
+
return {
|
|
5926
|
+
rowCount: 0,
|
|
5927
|
+
errored: true,
|
|
5928
|
+
__error: "No data provided for insert",
|
|
5929
|
+
__success: false
|
|
5930
|
+
};
|
|
6005
5931
|
}
|
|
6006
|
-
|
|
6007
|
-
const
|
|
5932
|
+
const pool = this.getPoolOrThrow(registration);
|
|
5933
|
+
const safetyPolicy = this.resolveSafetyPolicy(registration);
|
|
6008
5934
|
try {
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
}
|
|
6020
|
-
if (value.__effect === "decrement") {
|
|
6021
|
-
return `${Object.keys(row)[i]} - 1`;
|
|
6022
|
-
}
|
|
6023
|
-
if (value.__effect === "set") {
|
|
6024
|
-
return `${Object.keys(row)[i]} = ${value.__value}`;
|
|
6025
|
-
}
|
|
5935
|
+
const resultContext = await this.runWithRetries(
|
|
5936
|
+
async () => this.executeWithTransaction(pool, Boolean(transaction), async (client) => {
|
|
5937
|
+
const resolvedData = await this.resolveNestedData(
|
|
5938
|
+
registration,
|
|
5939
|
+
data,
|
|
5940
|
+
tableName
|
|
5941
|
+
);
|
|
5942
|
+
const rows = Array.isArray(resolvedData) ? resolvedData : [resolvedData];
|
|
5943
|
+
if (rows.length === 0) {
|
|
5944
|
+
throw new Error("No rows available for insert after resolving data");
|
|
6026
5945
|
}
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
5946
|
+
const keys = Object.keys(rows[0]);
|
|
5947
|
+
const sqlPrefix = `INSERT INTO ${tableName} (${keys.map((key) => (0, import_lodash_es.snakeCase)(key)).join(", ")}) VALUES `;
|
|
5948
|
+
const params = [];
|
|
5949
|
+
const placeholders = rows.map((row) => {
|
|
5950
|
+
const tuple = keys.map((key) => {
|
|
5951
|
+
params.push(row[key]);
|
|
5952
|
+
return `$${params.length}`;
|
|
5953
|
+
}).join(", ");
|
|
5954
|
+
return `(${tuple})`;
|
|
5955
|
+
}).join(", ");
|
|
5956
|
+
let onConflictSql = "";
|
|
5957
|
+
if (onConflict) {
|
|
5958
|
+
onConflictSql = this.buildOnConflictClause(onConflict, params);
|
|
6038
5959
|
}
|
|
6039
|
-
const
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
(v) => typeof v !== "string" || !v.startsWith("excluded.")
|
|
6045
|
-
)
|
|
5960
|
+
const sql = `${sqlPrefix}${placeholders}${onConflictSql} RETURNING ${fields.length ? fields.map(import_lodash_es.snakeCase).join(", ") : "*"}`;
|
|
5961
|
+
const result = await this.withTimeout(
|
|
5962
|
+
() => client.query(sql, params),
|
|
5963
|
+
safetyPolicy.statementTimeoutMs,
|
|
5964
|
+
`Insert timeout on table ${tableName}`
|
|
6046
5965
|
);
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
5966
|
+
const resultRows = this.toCamelCase(result.rows);
|
|
5967
|
+
return {
|
|
5968
|
+
[`${(0, import_lodash_es.camelCase)(tableName)}${rows.length > 1 ? "s" : ""}`]: rows.length > 1 ? resultRows : resultRows[0] ?? null,
|
|
5969
|
+
rowCount: result.rowCount,
|
|
5970
|
+
__success: true
|
|
5971
|
+
};
|
|
5972
|
+
}),
|
|
5973
|
+
safetyPolicy,
|
|
5974
|
+
`Insert ${tableName}`
|
|
6056
5975
|
);
|
|
6057
|
-
|
|
6058
|
-
const resultRows = this.toCamelCase(result.rows);
|
|
6059
|
-
resultContext = {
|
|
6060
|
-
[`${(0, import_lodash_es.camelCase)(tableName)}${isBatch ? "s" : ""}`]: isBatch ? resultRows : resultRows[0],
|
|
6061
|
-
rowCount: result.rowCount,
|
|
6062
|
-
__success: true
|
|
6063
|
-
};
|
|
5976
|
+
return resultContext;
|
|
6064
5977
|
} catch (error) {
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
} else {
|
|
6072
|
-
resultContext = {
|
|
6073
|
-
...context,
|
|
6074
|
-
errored: true,
|
|
6075
|
-
__error: `Insert failed: ${error.message}`,
|
|
6076
|
-
__success: false
|
|
6077
|
-
};
|
|
6078
|
-
}
|
|
6079
|
-
} finally {
|
|
6080
|
-
if (transaction && client) {
|
|
6081
|
-
client.release();
|
|
6082
|
-
}
|
|
5978
|
+
return {
|
|
5979
|
+
rowCount: 0,
|
|
5980
|
+
errored: true,
|
|
5981
|
+
__error: `Insert failed: ${errorMessage(error)}`,
|
|
5982
|
+
__success: false
|
|
5983
|
+
};
|
|
6083
5984
|
}
|
|
6084
|
-
return resultContext;
|
|
6085
5985
|
}
|
|
6086
|
-
|
|
6087
|
-
* Updates a database table with the provided data and filter conditions.
|
|
6088
|
-
*
|
|
6089
|
-
* @param {string} tableName - The name of the database table to update.
|
|
6090
|
-
* @param {DbOperationPayload} context - The payload for the update operation, which includes:
|
|
6091
|
-
* - data: The data to update in the table.
|
|
6092
|
-
* - filter: The conditions to identify the rows to update (default is an empty object).
|
|
6093
|
-
* - transaction: Whether the operation should run within a database transaction (default is true).
|
|
6094
|
-
* @return {Promise<any>} Returns a Promise resolving to an object that includes:
|
|
6095
|
-
* - The updated data if the update is successful.
|
|
6096
|
-
* - In case of error:
|
|
6097
|
-
* - Error details.
|
|
6098
|
-
* - The SQL query and parameters if applicable.
|
|
6099
|
-
* - A flag indicating if the update succeeded or failed.
|
|
6100
|
-
*/
|
|
6101
|
-
async updateFunction(tableName, context) {
|
|
5986
|
+
async updateFunction(registration, tableName, context) {
|
|
6102
5987
|
const { data, filter = {}, transaction = true } = context;
|
|
6103
5988
|
if (!data || Object.keys(data).length === 0) {
|
|
6104
5989
|
return {
|
|
5990
|
+
rowCount: 0,
|
|
6105
5991
|
errored: true,
|
|
6106
|
-
__error: `No data provided for update of ${tableName}
|
|
5992
|
+
__error: `No data provided for update of ${tableName}`,
|
|
5993
|
+
__success: false
|
|
6107
5994
|
};
|
|
6108
5995
|
}
|
|
6109
|
-
|
|
6110
|
-
const
|
|
5996
|
+
const pool = this.getPoolOrThrow(registration);
|
|
5997
|
+
const safetyPolicy = this.resolveSafetyPolicy(registration);
|
|
6111
5998
|
try {
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
5999
|
+
return await this.runWithRetries(
|
|
6000
|
+
async () => this.executeWithTransaction(pool, Boolean(transaction), async (client) => {
|
|
6001
|
+
const resolvedData = await this.resolveNestedData(
|
|
6002
|
+
registration,
|
|
6003
|
+
data,
|
|
6004
|
+
tableName
|
|
6005
|
+
);
|
|
6006
|
+
const params = Object.values(resolvedData);
|
|
6007
|
+
let offset = 0;
|
|
6008
|
+
const setClause = Object.keys(resolvedData).map((key, index) => {
|
|
6009
|
+
const value = resolvedData[key];
|
|
6010
|
+
const offsetIndex = index - offset;
|
|
6011
|
+
if (value?.__effect === "increment") {
|
|
6012
|
+
params.splice(offsetIndex, 1);
|
|
6013
|
+
offset += 1;
|
|
6014
|
+
return `${(0, import_lodash_es.snakeCase)(key)} = ${(0, import_lodash_es.snakeCase)(key)} + 1`;
|
|
6015
|
+
}
|
|
6016
|
+
if (value?.__effect === "decrement") {
|
|
6017
|
+
params.splice(offsetIndex, 1);
|
|
6018
|
+
offset += 1;
|
|
6019
|
+
return `${(0, import_lodash_es.snakeCase)(key)} = ${(0, import_lodash_es.snakeCase)(key)} - 1`;
|
|
6020
|
+
}
|
|
6021
|
+
if (value?.__effect === "set") {
|
|
6022
|
+
params.splice(offsetIndex, 1);
|
|
6023
|
+
offset += 1;
|
|
6024
|
+
return `${(0, import_lodash_es.snakeCase)(key)} = ${value.__value}`;
|
|
6025
|
+
}
|
|
6026
|
+
return `${(0, import_lodash_es.snakeCase)(key)} = $${offsetIndex + 1}`;
|
|
6027
|
+
}).join(", ");
|
|
6028
|
+
const whereClause = this.buildWhereClause(filter, params);
|
|
6029
|
+
const sql = `UPDATE ${tableName} SET ${setClause} ${whereClause} RETURNING *`;
|
|
6030
|
+
const result = await this.withTimeout(
|
|
6031
|
+
() => client.query(sql, params),
|
|
6032
|
+
safetyPolicy.statementTimeoutMs,
|
|
6033
|
+
`Update timeout on table ${tableName}`
|
|
6034
|
+
);
|
|
6035
|
+
const rows = this.toCamelCase(result.rows);
|
|
6036
|
+
const rowCount = Number(result.rowCount ?? 0);
|
|
6037
|
+
return {
|
|
6038
|
+
[`${(0, import_lodash_es.camelCase)(tableName)}`]: rows[0] ?? null,
|
|
6039
|
+
rowCount,
|
|
6040
|
+
__success: rowCount > 0
|
|
6041
|
+
};
|
|
6042
|
+
}),
|
|
6043
|
+
safetyPolicy,
|
|
6044
|
+
`Update ${tableName}`
|
|
6045
|
+
);
|
|
6153
6046
|
} catch (error) {
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
...context,
|
|
6047
|
+
return {
|
|
6048
|
+
rowCount: 0,
|
|
6157
6049
|
errored: true,
|
|
6158
|
-
__error: `Update failed: ${error
|
|
6050
|
+
__error: `Update failed: ${errorMessage(error)}`,
|
|
6159
6051
|
__success: false
|
|
6160
6052
|
};
|
|
6161
|
-
} finally {
|
|
6162
|
-
if (transaction && client) {
|
|
6163
|
-
client.release();
|
|
6164
|
-
}
|
|
6165
6053
|
}
|
|
6166
|
-
return resultContext;
|
|
6167
6054
|
}
|
|
6168
|
-
|
|
6169
|
-
* Deletes a record from the specified database table based on the given filter criteria.
|
|
6170
|
-
*
|
|
6171
|
-
* @param {string} tableName - The name of the database table from which records should be deleted.
|
|
6172
|
-
* @param {DbOperationPayload} context - The context for the operation, including filter conditions and transaction settings.
|
|
6173
|
-
* @param {Object} context.filter - The filter criteria to identify the records to delete.
|
|
6174
|
-
* @param {boolean} [context.transaction=true] - Indicates if the operation should be executed within a transaction.
|
|
6175
|
-
* @return {Promise<any>} A promise that resolves to an object containing information about the deleted record
|
|
6176
|
-
* or an error object if the delete operation fails.
|
|
6177
|
-
*/
|
|
6178
|
-
async deleteFunction(tableName, context) {
|
|
6055
|
+
async deleteFunction(registration, tableName, context) {
|
|
6179
6056
|
const { filter = {}, transaction = true } = context;
|
|
6180
6057
|
if (Object.keys(filter).length === 0) {
|
|
6181
|
-
return {
|
|
6058
|
+
return {
|
|
6059
|
+
rowCount: 0,
|
|
6060
|
+
errored: true,
|
|
6061
|
+
__error: "No filter provided for delete",
|
|
6062
|
+
__success: false
|
|
6063
|
+
};
|
|
6182
6064
|
}
|
|
6183
|
-
|
|
6184
|
-
const
|
|
6065
|
+
const pool = this.getPoolOrThrow(registration);
|
|
6066
|
+
const safetyPolicy = this.resolveSafetyPolicy(registration);
|
|
6185
6067
|
try {
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6068
|
+
return await this.runWithRetries(
|
|
6069
|
+
async () => this.executeWithTransaction(pool, Boolean(transaction), async (client) => {
|
|
6070
|
+
const params = [];
|
|
6071
|
+
const whereClause = this.buildWhereClause(filter, params);
|
|
6072
|
+
const sql = `DELETE FROM ${tableName} ${whereClause} RETURNING *`;
|
|
6073
|
+
const result = await this.withTimeout(
|
|
6074
|
+
() => client.query(sql, params),
|
|
6075
|
+
safetyPolicy.statementTimeoutMs,
|
|
6076
|
+
`Delete timeout on table ${tableName}`
|
|
6077
|
+
);
|
|
6078
|
+
const rows = this.toCamelCase(result.rows);
|
|
6079
|
+
return {
|
|
6080
|
+
[`${(0, import_lodash_es.camelCase)(tableName)}`]: rows[0] ?? null,
|
|
6081
|
+
rowCount: result.rowCount,
|
|
6082
|
+
__success: true
|
|
6083
|
+
};
|
|
6084
|
+
}),
|
|
6085
|
+
safetyPolicy,
|
|
6086
|
+
`Delete ${tableName}`
|
|
6087
|
+
);
|
|
6197
6088
|
} catch (error) {
|
|
6198
|
-
|
|
6199
|
-
|
|
6089
|
+
return {
|
|
6090
|
+
rowCount: 0,
|
|
6200
6091
|
errored: true,
|
|
6201
|
-
__error: `Delete failed: ${error
|
|
6202
|
-
__errors: { delete: error.message },
|
|
6092
|
+
__error: `Delete failed: ${errorMessage(error)}`,
|
|
6203
6093
|
__success: false
|
|
6204
6094
|
};
|
|
6205
|
-
}
|
|
6206
|
-
|
|
6207
|
-
|
|
6095
|
+
}
|
|
6096
|
+
}
|
|
6097
|
+
resolveSafetyPolicy(registration) {
|
|
6098
|
+
const durableState = registration.actor.getState(
|
|
6099
|
+
registration.actorKey
|
|
6100
|
+
);
|
|
6101
|
+
return {
|
|
6102
|
+
statementTimeoutMs: normalizePositiveInteger(
|
|
6103
|
+
durableState.safetyPolicy?.statementTimeoutMs,
|
|
6104
|
+
15e3
|
|
6105
|
+
),
|
|
6106
|
+
retryCount: normalizePositiveInteger(durableState.safetyPolicy?.retryCount, 3, 0),
|
|
6107
|
+
retryDelayMs: normalizePositiveInteger(durableState.safetyPolicy?.retryDelayMs, 100),
|
|
6108
|
+
retryDelayMaxMs: normalizePositiveInteger(
|
|
6109
|
+
durableState.safetyPolicy?.retryDelayMaxMs,
|
|
6110
|
+
1e3
|
|
6111
|
+
),
|
|
6112
|
+
retryDelayFactor: Number.isFinite(durableState.safetyPolicy?.retryDelayFactor) ? Math.max(1, Number(durableState.safetyPolicy?.retryDelayFactor)) : 1
|
|
6113
|
+
};
|
|
6114
|
+
}
|
|
6115
|
+
buildOnConflictClause(onConflict, params) {
|
|
6116
|
+
const { target, action } = onConflict;
|
|
6117
|
+
let sql = ` ON CONFLICT (${target.map(import_lodash_es.snakeCase).join(", ")})`;
|
|
6118
|
+
if (action.do === "update") {
|
|
6119
|
+
if (!action.set || Object.keys(action.set).length === 0) {
|
|
6120
|
+
throw new Error("Update action requires 'set' fields");
|
|
6121
|
+
}
|
|
6122
|
+
const assignments = Object.entries(action.set).map(([field, value]) => {
|
|
6123
|
+
if (typeof value === "string" && value === "excluded") {
|
|
6124
|
+
return `${(0, import_lodash_es.snakeCase)(field)} = excluded.${(0, import_lodash_es.snakeCase)(field)}`;
|
|
6125
|
+
}
|
|
6126
|
+
params.push(value);
|
|
6127
|
+
return `${(0, import_lodash_es.snakeCase)(field)} = $${params.length}`;
|
|
6128
|
+
});
|
|
6129
|
+
sql += ` DO UPDATE SET ${assignments.join(", ")}`;
|
|
6130
|
+
if (action.where) {
|
|
6131
|
+
sql += ` WHERE ${action.where}`;
|
|
6208
6132
|
}
|
|
6133
|
+
return sql;
|
|
6209
6134
|
}
|
|
6210
|
-
|
|
6135
|
+
sql += " DO NOTHING";
|
|
6136
|
+
return sql;
|
|
6211
6137
|
}
|
|
6212
6138
|
/**
|
|
6213
|
-
*
|
|
6214
|
-
* Builds parameterized queries to prevent SQL injection, appending parameters
|
|
6215
|
-
* to the provided params array and utilizing placeholders.
|
|
6216
|
-
*
|
|
6217
|
-
* @param {Object} filter - An object representing the filtering conditions with
|
|
6218
|
-
* keys as column names and values as their corresponding
|
|
6219
|
-
* desired values. Values can also be arrays for `IN` queries.
|
|
6220
|
-
* @param {any[]} params - An array for storing parameterized values, which will be
|
|
6221
|
-
* populated with the filter values for the constructed SQL clause.
|
|
6222
|
-
* @return {string} The constructed SQL WHERE clause as a string. If no conditions
|
|
6223
|
-
* are provided, an empty string is returned.
|
|
6139
|
+
* Validates database schema structure and content.
|
|
6224
6140
|
*/
|
|
6225
|
-
|
|
6226
|
-
const
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6141
|
+
validateSchema(ctx) {
|
|
6142
|
+
const schema = ctx.schema;
|
|
6143
|
+
if (!schema?.tables || typeof schema.tables !== "object") {
|
|
6144
|
+
throw new Error("Invalid schema: missing or invalid tables");
|
|
6145
|
+
}
|
|
6146
|
+
for (const [tableName, table] of Object.entries(schema.tables)) {
|
|
6147
|
+
if (!isSqlIdentifier(tableName)) {
|
|
6148
|
+
throw new Error(
|
|
6149
|
+
`Invalid table name ${tableName}. Table names must use lowercase snake_case identifiers.`
|
|
6150
|
+
);
|
|
6151
|
+
}
|
|
6152
|
+
if (!table.fields || typeof table.fields !== "object") {
|
|
6153
|
+
throw new Error(`Invalid table ${tableName}: missing fields`);
|
|
6154
|
+
}
|
|
6155
|
+
for (const [fieldName, field] of Object.entries(table.fields)) {
|
|
6156
|
+
if (!isSqlIdentifier(fieldName)) {
|
|
6157
|
+
throw new Error(
|
|
6158
|
+
`Invalid field name ${fieldName} for ${tableName}. Field names must use lowercase snake_case identifiers.`
|
|
6159
|
+
);
|
|
6160
|
+
}
|
|
6161
|
+
if (!SCHEMA_TYPES.includes(field.type)) {
|
|
6162
|
+
throw new Error(`Invalid type ${field.type} for ${tableName}.${fieldName}`);
|
|
6163
|
+
}
|
|
6164
|
+
if (field.references && !field.references.match(/^[\w]+\([\w]+\)$/)) {
|
|
6165
|
+
throw new Error(
|
|
6166
|
+
`Invalid reference ${field.references} for ${tableName}.${fieldName}`
|
|
6236
6167
|
);
|
|
6237
|
-
}
|
|
6238
|
-
|
|
6239
|
-
|
|
6168
|
+
}
|
|
6169
|
+
}
|
|
6170
|
+
for (const operation of ["query", "insert", "update", "delete"]) {
|
|
6171
|
+
const customIntents = table.customIntents?.[operation] ?? [];
|
|
6172
|
+
if (!Array.isArray(customIntents)) {
|
|
6173
|
+
throw new Error(
|
|
6174
|
+
`Invalid customIntents.${operation} for table ${tableName}: expected array`
|
|
6175
|
+
);
|
|
6176
|
+
}
|
|
6177
|
+
for (const customIntent of customIntents) {
|
|
6178
|
+
const parsed = readCustomIntentConfig(
|
|
6179
|
+
customIntent
|
|
6180
|
+
);
|
|
6181
|
+
validateIntentName(String(parsed.intent ?? ""));
|
|
6240
6182
|
}
|
|
6241
6183
|
}
|
|
6242
6184
|
}
|
|
6243
|
-
return
|
|
6185
|
+
return true;
|
|
6244
6186
|
}
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6187
|
+
sortTablesByReferences(ctx) {
|
|
6188
|
+
const schema = ctx.schema;
|
|
6189
|
+
const graph = /* @__PURE__ */ new Map();
|
|
6190
|
+
const allTables = Object.keys(schema.tables);
|
|
6191
|
+
allTables.forEach((table) => graph.set(table, /* @__PURE__ */ new Set()));
|
|
6192
|
+
for (const [tableName, table] of Object.entries(schema.tables)) {
|
|
6193
|
+
for (const field of Object.values(table.fields)) {
|
|
6194
|
+
if (field.references) {
|
|
6195
|
+
const [refTable] = field.references.split("(");
|
|
6196
|
+
if (refTable !== tableName && allTables.includes(refTable)) {
|
|
6197
|
+
graph.get(refTable)?.add(tableName);
|
|
6198
|
+
}
|
|
6199
|
+
}
|
|
6200
|
+
}
|
|
6201
|
+
if (table.foreignKeys) {
|
|
6202
|
+
for (const foreignKey of table.foreignKeys) {
|
|
6203
|
+
const refTable = foreignKey.tableName;
|
|
6204
|
+
if (refTable !== tableName && allTables.includes(refTable)) {
|
|
6205
|
+
graph.get(refTable)?.add(tableName);
|
|
6206
|
+
}
|
|
6207
|
+
}
|
|
6208
|
+
}
|
|
6257
6209
|
}
|
|
6258
|
-
|
|
6210
|
+
const visited = /* @__PURE__ */ new Set();
|
|
6211
|
+
const tempMark = /* @__PURE__ */ new Set();
|
|
6212
|
+
const sorted = [];
|
|
6213
|
+
let hasCycles = false;
|
|
6214
|
+
const visit = (table) => {
|
|
6215
|
+
if (tempMark.has(table)) {
|
|
6216
|
+
hasCycles = true;
|
|
6217
|
+
return;
|
|
6218
|
+
}
|
|
6219
|
+
if (visited.has(table)) return;
|
|
6220
|
+
tempMark.add(table);
|
|
6221
|
+
for (const dependent of graph.get(table) || []) {
|
|
6222
|
+
visit(dependent);
|
|
6223
|
+
}
|
|
6224
|
+
tempMark.delete(table);
|
|
6225
|
+
visited.add(table);
|
|
6226
|
+
sorted.push(table);
|
|
6227
|
+
};
|
|
6228
|
+
for (const table of allTables) {
|
|
6229
|
+
if (!visited.has(table)) {
|
|
6230
|
+
visit(table);
|
|
6231
|
+
}
|
|
6232
|
+
}
|
|
6233
|
+
for (const table of allTables) {
|
|
6234
|
+
if (!visited.has(table)) {
|
|
6235
|
+
sorted.push(table);
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
6238
|
+
sorted.reverse();
|
|
6239
|
+
return { ...ctx, sortedTables: sorted, hasCycles };
|
|
6259
6240
|
}
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6241
|
+
buildSchemaDdlStatements(schema, sortedTables) {
|
|
6242
|
+
const ddl = [];
|
|
6243
|
+
for (const tableName of sortedTables) {
|
|
6244
|
+
const table = schema.tables[tableName];
|
|
6245
|
+
const fieldDefs = Object.entries(table.fields).map(([fieldName, field]) => this.fieldDefinitionToSql(fieldName, field)).join(", ");
|
|
6246
|
+
ddl.push(`CREATE TABLE IF NOT EXISTS ${tableName} (${fieldDefs});`);
|
|
6247
|
+
for (const indexFields of table.indexes ?? []) {
|
|
6248
|
+
ddl.push(
|
|
6249
|
+
`CREATE INDEX IF NOT EXISTS idx_${tableName}_${indexFields.join("_")} ON ${tableName} (${indexFields.map(import_lodash_es.snakeCase).join(", ")});`
|
|
6250
|
+
);
|
|
6251
|
+
}
|
|
6252
|
+
if (table.primaryKey) {
|
|
6253
|
+
ddl.push(
|
|
6254
|
+
`ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS pk_${tableName}_${table.primaryKey.join("_")};`,
|
|
6255
|
+
`ALTER TABLE ${tableName} ADD CONSTRAINT pk_${tableName}_${table.primaryKey.join("_")} PRIMARY KEY (${table.primaryKey.map(import_lodash_es.snakeCase).join(", ")});`
|
|
6256
|
+
);
|
|
6257
|
+
}
|
|
6258
|
+
for (const uniqueFields of table.uniqueConstraints ?? []) {
|
|
6259
|
+
ddl.push(
|
|
6260
|
+
`ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS uq_${tableName}_${uniqueFields.join("_")};`,
|
|
6261
|
+
`ALTER TABLE ${tableName} ADD CONSTRAINT uq_${tableName}_${uniqueFields.join("_")} UNIQUE (${uniqueFields.map(import_lodash_es.snakeCase).join(", ")});`
|
|
6262
|
+
);
|
|
6263
|
+
}
|
|
6264
|
+
for (const foreignKey of table.foreignKeys ?? []) {
|
|
6265
|
+
const fkName = `fk_${tableName}_${foreignKey.fields.join("_")}`;
|
|
6266
|
+
ddl.push(
|
|
6267
|
+
`ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS ${fkName};`,
|
|
6268
|
+
`ALTER TABLE ${tableName} ADD CONSTRAINT ${fkName} FOREIGN KEY (${foreignKey.fields.map(import_lodash_es.snakeCase).join(", ")}) REFERENCES ${foreignKey.tableName} (${foreignKey.referenceFields.map(import_lodash_es.snakeCase).join(", ")});`
|
|
6269
|
+
);
|
|
6270
|
+
}
|
|
6271
|
+
for (const [triggerName, trigger] of Object.entries(table.triggers ?? {})) {
|
|
6272
|
+
ddl.push(
|
|
6273
|
+
`CREATE OR REPLACE TRIGGER ${triggerName} ${trigger.when} ${trigger.event} ON ${tableName} FOR EACH STATEMENT EXECUTE FUNCTION ${trigger.function};`
|
|
6274
|
+
);
|
|
6275
|
+
}
|
|
6276
|
+
if (table.initialData) {
|
|
6277
|
+
ddl.push(
|
|
6278
|
+
`INSERT INTO ${tableName} (${table.initialData.fields.map(import_lodash_es.snakeCase).join(", ")}) VALUES ${table.initialData.data.map(
|
|
6279
|
+
(row) => `(${row.map((value) => {
|
|
6280
|
+
if (value === void 0) return "NULL";
|
|
6281
|
+
if (value === null) return "NULL";
|
|
6282
|
+
if (typeof value === "number") return String(value);
|
|
6283
|
+
if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
|
|
6284
|
+
const stringValue = String(value);
|
|
6285
|
+
return `'${stringValue.replace(/'/g, "''")}'`;
|
|
6286
|
+
}).join(", ")})`
|
|
6287
|
+
).join(", ")} ON CONFLICT DO NOTHING;`
|
|
6288
|
+
);
|
|
6281
6289
|
}
|
|
6282
6290
|
}
|
|
6283
|
-
return
|
|
6291
|
+
return ddl;
|
|
6284
6292
|
}
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6293
|
+
fieldDefinitionToSql(fieldName, field) {
|
|
6294
|
+
let definition = `${(0, import_lodash_es.snakeCase)(fieldName)} ${field.type.toUpperCase()}`;
|
|
6295
|
+
if (field.type === "varchar") {
|
|
6296
|
+
definition += `(${field.constraints?.maxLength ?? 255})`;
|
|
6297
|
+
}
|
|
6298
|
+
if (field.type === "decimal") {
|
|
6299
|
+
definition += `(${field.constraints?.precision ?? 10},${field.constraints?.scale ?? 2})`;
|
|
6300
|
+
}
|
|
6301
|
+
if (field.primary) definition += " PRIMARY KEY";
|
|
6302
|
+
if (field.unique) definition += " UNIQUE";
|
|
6303
|
+
if (field.default !== void 0) {
|
|
6304
|
+
definition += ` DEFAULT ${field.default === "" ? "''" : String(field.default)}`;
|
|
6305
|
+
}
|
|
6306
|
+
if (field.required && !field.nullable) definition += " NOT NULL";
|
|
6307
|
+
if (field.nullable) definition += " NULL";
|
|
6308
|
+
if (field.generated) {
|
|
6309
|
+
definition += ` GENERATED ALWAYS AS ${field.generated.toUpperCase()} STORED`;
|
|
6310
|
+
}
|
|
6311
|
+
if (field.references) {
|
|
6312
|
+
definition += ` REFERENCES ${field.references} ON DELETE ${field.onDelete || "CASCADE"}`;
|
|
6313
|
+
}
|
|
6314
|
+
if (field.constraints?.check) {
|
|
6315
|
+
definition += ` CHECK (${field.constraints.check})`;
|
|
6316
|
+
}
|
|
6317
|
+
return definition;
|
|
6318
|
+
}
|
|
6319
|
+
async applyDdlStatements(pool, statements) {
|
|
6320
|
+
for (const sql of statements) {
|
|
6321
|
+
try {
|
|
6322
|
+
await pool.query(sql);
|
|
6323
|
+
} catch (error) {
|
|
6324
|
+
CadenzaService.log(
|
|
6325
|
+
"Error applying DDL statement",
|
|
6326
|
+
{
|
|
6327
|
+
sql,
|
|
6328
|
+
error: errorMessage(error)
|
|
6329
|
+
},
|
|
6330
|
+
"error"
|
|
6331
|
+
);
|
|
6332
|
+
throw error;
|
|
6314
6333
|
}
|
|
6315
|
-
await client.query("COMMIT");
|
|
6316
|
-
return result || {};
|
|
6317
|
-
} catch (error) {
|
|
6318
|
-
await client.query("ROLLBACK");
|
|
6319
|
-
throw error;
|
|
6320
|
-
} finally {
|
|
6321
|
-
client.release();
|
|
6322
6334
|
}
|
|
6323
6335
|
}
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
const
|
|
6336
|
-
const
|
|
6336
|
+
generateDatabaseTasks(registration) {
|
|
6337
|
+
for (const [tableName, table] of Object.entries(registration.schema.tables)) {
|
|
6338
|
+
this.createDatabaseTask(registration, "query", tableName, table);
|
|
6339
|
+
this.createDatabaseTask(registration, "insert", tableName, table);
|
|
6340
|
+
this.createDatabaseTask(registration, "update", tableName, table);
|
|
6341
|
+
this.createDatabaseTask(registration, "delete", tableName, table);
|
|
6342
|
+
this.createDatabaseMacroTasks(registration, tableName, table);
|
|
6343
|
+
}
|
|
6344
|
+
}
|
|
6345
|
+
createDatabaseMacroTasks(registration, tableName, table) {
|
|
6346
|
+
const querySchema = this.getInputSchema("query", tableName, table);
|
|
6347
|
+
const insertSchema = this.getInputSchema("insert", tableName, table);
|
|
6348
|
+
const queryMacroOperations = [
|
|
6349
|
+
"count",
|
|
6350
|
+
"exists",
|
|
6351
|
+
"one",
|
|
6352
|
+
"aggregate"
|
|
6353
|
+
];
|
|
6354
|
+
for (const macroOperation of queryMacroOperations) {
|
|
6355
|
+
const intentName = `${macroOperation}-pg-${registration.actorToken}-${tableName}`;
|
|
6356
|
+
if (registration.intentNames.has(intentName)) {
|
|
6357
|
+
throw new Error(
|
|
6358
|
+
`Duplicate macro intent '${intentName}' detected for table '${tableName}' in actor '${registration.actorName}'`
|
|
6359
|
+
);
|
|
6360
|
+
}
|
|
6361
|
+
registration.intentNames.add(intentName);
|
|
6362
|
+
CadenzaService.defineIntent({
|
|
6363
|
+
name: intentName,
|
|
6364
|
+
description: `Macro ${macroOperation} operation for table ${tableName}`,
|
|
6365
|
+
input: querySchema
|
|
6366
|
+
});
|
|
6367
|
+
CadenzaService.createThrottledTask(
|
|
6368
|
+
`${macroOperation.toUpperCase()} ${tableName}`,
|
|
6369
|
+
registration.actor.task(
|
|
6370
|
+
async ({ input }) => {
|
|
6371
|
+
const payload = typeof input.queryData === "object" && input.queryData ? input.queryData : input;
|
|
6372
|
+
const result = await this.queryFunction(registration, tableName, {
|
|
6373
|
+
...payload,
|
|
6374
|
+
queryMode: macroOperation
|
|
6375
|
+
});
|
|
6376
|
+
return {
|
|
6377
|
+
...input,
|
|
6378
|
+
...result
|
|
6379
|
+
};
|
|
6380
|
+
},
|
|
6381
|
+
{ mode: "read" }
|
|
6382
|
+
),
|
|
6383
|
+
(context) => context?.__metadata?.__executionTraceId ?? context?.__executionTraceId ?? "default",
|
|
6384
|
+
`Macro ${macroOperation} task for ${tableName}`,
|
|
6385
|
+
{
|
|
6386
|
+
isMeta: registration.options.isMeta,
|
|
6387
|
+
isSubMeta: registration.options.isMeta,
|
|
6388
|
+
validateInputContext: registration.options.securityProfile !== "low",
|
|
6389
|
+
inputSchema: querySchema
|
|
6390
|
+
}
|
|
6391
|
+
).respondsTo(intentName);
|
|
6392
|
+
}
|
|
6393
|
+
const upsertIntentName = `upsert-pg-${registration.actorToken}-${tableName}`;
|
|
6394
|
+
if (registration.intentNames.has(upsertIntentName)) {
|
|
6395
|
+
throw new Error(
|
|
6396
|
+
`Duplicate macro intent '${upsertIntentName}' detected for table '${tableName}' in actor '${registration.actorName}'`
|
|
6397
|
+
);
|
|
6398
|
+
}
|
|
6399
|
+
registration.intentNames.add(upsertIntentName);
|
|
6400
|
+
CadenzaService.defineIntent({
|
|
6401
|
+
name: upsertIntentName,
|
|
6402
|
+
description: `Macro upsert operation for table ${tableName}`,
|
|
6403
|
+
input: insertSchema
|
|
6404
|
+
});
|
|
6405
|
+
CadenzaService.createThrottledTask(
|
|
6406
|
+
`UPSERT ${tableName}`,
|
|
6407
|
+
registration.actor.task(
|
|
6408
|
+
async ({ input }) => {
|
|
6409
|
+
const payload = typeof input.queryData === "object" && input.queryData ? input.queryData : input;
|
|
6410
|
+
if (!payload.onConflict) {
|
|
6411
|
+
return {
|
|
6412
|
+
...input,
|
|
6413
|
+
errored: true,
|
|
6414
|
+
__success: false,
|
|
6415
|
+
__error: `Macro upsert requires 'onConflict' payload for table '${tableName}'`
|
|
6416
|
+
};
|
|
6417
|
+
}
|
|
6418
|
+
const result = await this.insertFunction(registration, tableName, payload);
|
|
6419
|
+
return {
|
|
6420
|
+
...input,
|
|
6421
|
+
...result
|
|
6422
|
+
};
|
|
6423
|
+
},
|
|
6424
|
+
{ mode: "write" }
|
|
6425
|
+
),
|
|
6426
|
+
(context) => context?.__metadata?.__executionTraceId ?? context?.__executionTraceId ?? "default",
|
|
6427
|
+
`Macro upsert task for ${tableName}`,
|
|
6428
|
+
{
|
|
6429
|
+
isMeta: registration.options.isMeta,
|
|
6430
|
+
isSubMeta: registration.options.isMeta,
|
|
6431
|
+
validateInputContext: registration.options.securityProfile !== "low",
|
|
6432
|
+
inputSchema: insertSchema
|
|
6433
|
+
}
|
|
6434
|
+
).respondsTo(upsertIntentName);
|
|
6435
|
+
}
|
|
6436
|
+
createDatabaseTask(registration, op, tableName, table) {
|
|
6437
|
+
const opAction = op === "query" ? "queried" : op === "insert" ? "inserted" : op === "update" ? "updated" : "deleted";
|
|
6438
|
+
const defaultSignal = `global.${registration.options.isMeta ? "meta." : ""}${tableName}.${opAction}`;
|
|
6337
6439
|
const taskName = `${op.charAt(0).toUpperCase() + op.slice(1)} ${tableName}`;
|
|
6338
6440
|
const schema = this.getInputSchema(op, tableName, table);
|
|
6339
|
-
const
|
|
6340
|
-
|
|
6341
|
-
|
|
6441
|
+
const databaseTaskFunction = registration.actor.task(
|
|
6442
|
+
async ({ input, emit }) => {
|
|
6443
|
+
let context = { ...input };
|
|
6444
|
+
let payloadModifiedByTriggers = false;
|
|
6342
6445
|
for (const action of Object.keys(table.customSignals?.triggers ?? {})) {
|
|
6343
|
-
const
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
for (const triggerCondition of triggerConditions ?? []) {
|
|
6350
|
-
if (triggerCondition.condition && !triggerCondition.condition(context)) {
|
|
6446
|
+
const triggerDefinitions = table.customSignals?.triggers?.[action];
|
|
6447
|
+
for (const trigger of triggerDefinitions ?? []) {
|
|
6448
|
+
if (typeof trigger === "string") {
|
|
6449
|
+
continue;
|
|
6450
|
+
}
|
|
6451
|
+
if (trigger.condition && !trigger.condition(context)) {
|
|
6351
6452
|
return {
|
|
6352
6453
|
failed: true,
|
|
6353
|
-
|
|
6454
|
+
__success: false,
|
|
6455
|
+
__error: `Condition for signal trigger failed: ${trigger.signal}`
|
|
6354
6456
|
};
|
|
6355
6457
|
}
|
|
6356
|
-
|
|
6357
|
-
const triggerQueryData = (
|
|
6358
|
-
// @ts-ignore
|
|
6359
|
-
table.customSignals?.triggers?.[action].filter(
|
|
6360
|
-
(trigger) => trigger.queryData
|
|
6361
|
-
)
|
|
6362
|
-
);
|
|
6363
|
-
for (const queryData of triggerQueryData ?? []) {
|
|
6364
|
-
if (context.queryData) {
|
|
6458
|
+
if (trigger.queryData) {
|
|
6365
6459
|
context.queryData = {
|
|
6366
|
-
...context.queryData,
|
|
6367
|
-
...queryData
|
|
6368
|
-
};
|
|
6369
|
-
} else {
|
|
6370
|
-
context = {
|
|
6371
|
-
...context,
|
|
6372
|
-
...queryData
|
|
6460
|
+
...context.queryData ?? {},
|
|
6461
|
+
...trigger.queryData
|
|
6373
6462
|
};
|
|
6463
|
+
payloadModifiedByTriggers = true;
|
|
6374
6464
|
}
|
|
6375
6465
|
}
|
|
6376
6466
|
}
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6467
|
+
const operationPayload = typeof context.queryData === "object" && context.queryData ? context.queryData : context;
|
|
6468
|
+
this.validateOperationPayload(
|
|
6469
|
+
registration,
|
|
6470
|
+
op,
|
|
6471
|
+
tableName,
|
|
6472
|
+
table,
|
|
6473
|
+
operationPayload,
|
|
6474
|
+
{
|
|
6475
|
+
enforceFieldAllowlist: registration.options.securityProfile === "low" || payloadModifiedByTriggers
|
|
6476
|
+
}
|
|
6477
|
+
);
|
|
6478
|
+
let result;
|
|
6479
|
+
if (op === "query") {
|
|
6480
|
+
result = await this.queryFunction(registration, tableName, operationPayload);
|
|
6481
|
+
} else if (op === "insert") {
|
|
6482
|
+
result = await this.insertFunction(registration, tableName, operationPayload);
|
|
6483
|
+
} else if (op === "update") {
|
|
6484
|
+
result = await this.updateFunction(registration, tableName, operationPayload);
|
|
6485
|
+
} else {
|
|
6486
|
+
result = await this.deleteFunction(registration, tableName, operationPayload);
|
|
6393
6487
|
}
|
|
6488
|
+
context = {
|
|
6489
|
+
...context,
|
|
6490
|
+
...result
|
|
6491
|
+
};
|
|
6394
6492
|
if (!context.errored) {
|
|
6395
6493
|
for (const signal of table.customSignals?.emissions?.[op] ?? []) {
|
|
6494
|
+
if (typeof signal === "string") {
|
|
6495
|
+
emit(signal, context);
|
|
6496
|
+
continue;
|
|
6497
|
+
}
|
|
6396
6498
|
if (signal.condition && !signal.condition(context)) {
|
|
6397
6499
|
continue;
|
|
6398
6500
|
}
|
|
6399
|
-
emit(signal.signal
|
|
6501
|
+
emit(signal.signal, context);
|
|
6400
6502
|
}
|
|
6401
6503
|
}
|
|
6402
6504
|
if (tableName !== "system_log" && context.errored) {
|
|
@@ -6426,49 +6528,263 @@ var DatabaseController = class _DatabaseController {
|
|
|
6426
6528
|
delete context.offset;
|
|
6427
6529
|
return context;
|
|
6428
6530
|
},
|
|
6531
|
+
{ mode: op === "query" ? "read" : "write" }
|
|
6532
|
+
);
|
|
6533
|
+
const task = CadenzaService.createThrottledTask(
|
|
6534
|
+
taskName,
|
|
6535
|
+
databaseTaskFunction,
|
|
6429
6536
|
(context) => context?.__metadata?.__executionTraceId ?? context?.__executionTraceId ?? "default",
|
|
6430
|
-
`Auto-generated ${op} task for ${tableName}`,
|
|
6537
|
+
`Auto-generated ${op} task for ${tableName} (PostgresActor)`,
|
|
6431
6538
|
{
|
|
6432
|
-
isMeta: options.isMeta,
|
|
6433
|
-
isSubMeta: options.isMeta,
|
|
6434
|
-
validateInputContext: options.securityProfile !== "low",
|
|
6539
|
+
isMeta: registration.options.isMeta,
|
|
6540
|
+
isSubMeta: registration.options.isMeta,
|
|
6541
|
+
validateInputContext: registration.options.securityProfile !== "low",
|
|
6435
6542
|
inputSchema: schema
|
|
6436
6543
|
}
|
|
6437
|
-
).doOn(
|
|
6438
|
-
...table.customSignals?.triggers?.[op]?.map(
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
).emits(defaultSignal).attachSignal(
|
|
6442
|
-
...table.customSignals?.emissions?.[op]?.map(
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
);
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6544
|
+
).doOn(
|
|
6545
|
+
...table.customSignals?.triggers?.[op]?.map(
|
|
6546
|
+
(signal) => typeof signal === "string" ? signal : signal.signal
|
|
6547
|
+
) ?? []
|
|
6548
|
+
).emits(defaultSignal).attachSignal(
|
|
6549
|
+
...table.customSignals?.emissions?.[op]?.map(
|
|
6550
|
+
(signal) => typeof signal === "string" ? signal : signal.signal
|
|
6551
|
+
) ?? []
|
|
6552
|
+
);
|
|
6553
|
+
const { intents } = resolveTableOperationIntents(
|
|
6554
|
+
registration.actorName,
|
|
6555
|
+
tableName,
|
|
6556
|
+
table,
|
|
6557
|
+
op,
|
|
6558
|
+
schema
|
|
6559
|
+
);
|
|
6560
|
+
for (const intent of intents) {
|
|
6561
|
+
if (registration.intentNames.has(intent.name)) {
|
|
6562
|
+
throw new Error(
|
|
6563
|
+
`Duplicate auto/custom intent '${intent.name}' detected while generating ${op} task for table '${tableName}' in actor '${registration.actorName}'`
|
|
6564
|
+
);
|
|
6565
|
+
}
|
|
6566
|
+
registration.intentNames.add(intent.name);
|
|
6567
|
+
CadenzaService.defineIntent({
|
|
6568
|
+
name: intent.name,
|
|
6569
|
+
description: intent.description,
|
|
6570
|
+
input: intent.input
|
|
6571
|
+
});
|
|
6572
|
+
}
|
|
6573
|
+
task.respondsTo(...intents.map((intent) => intent.name));
|
|
6574
|
+
}
|
|
6575
|
+
validateOperationPayload(registration, operation, tableName, table, payload, options) {
|
|
6576
|
+
const allowedFields = new Set(Object.keys(table.fields));
|
|
6577
|
+
const resolvedMode = payload.queryMode ?? "rows";
|
|
6578
|
+
if (!["rows", "count", "exists", "one", "aggregate"].includes(resolvedMode)) {
|
|
6579
|
+
throw new Error(`Unsupported queryMode '${String(payload.queryMode)}'`);
|
|
6580
|
+
}
|
|
6581
|
+
const assertAllowedField = (fieldName, label) => {
|
|
6582
|
+
if (!allowedFields.has(fieldName)) {
|
|
6583
|
+
throw new Error(
|
|
6584
|
+
`Invalid field '${fieldName}' in ${label} for ${operation} on ${tableName}`
|
|
6585
|
+
);
|
|
6586
|
+
}
|
|
6587
|
+
};
|
|
6588
|
+
const aggregateDefinitions = Array.isArray(payload.aggregates) ? payload.aggregates : [];
|
|
6589
|
+
const aggregateSortAllowlist = /* @__PURE__ */ new Set();
|
|
6590
|
+
if (resolvedMode === "aggregate") {
|
|
6591
|
+
if (aggregateDefinitions.length === 0) {
|
|
6592
|
+
throw new Error(
|
|
6593
|
+
`Aggregate queryMode requires at least one aggregate on table '${tableName}'`
|
|
6594
|
+
);
|
|
6595
|
+
}
|
|
6596
|
+
for (const groupField of payload.groupBy ?? []) {
|
|
6597
|
+
assertAllowedField(groupField, "groupBy");
|
|
6598
|
+
aggregateSortAllowlist.add(groupField);
|
|
6599
|
+
}
|
|
6600
|
+
for (const [index, aggregate] of aggregateDefinitions.entries()) {
|
|
6601
|
+
if (!isSupportedAggregateFunction(aggregate.fn)) {
|
|
6602
|
+
throw new Error(
|
|
6603
|
+
`Unsupported aggregate function '${String(aggregate.fn)}' on table '${tableName}'`
|
|
6604
|
+
);
|
|
6605
|
+
}
|
|
6606
|
+
if (aggregate.fn !== "count" && !aggregate.field) {
|
|
6607
|
+
throw new Error(
|
|
6608
|
+
`Aggregate '${aggregate.fn}' requires field on table '${tableName}'`
|
|
6609
|
+
);
|
|
6610
|
+
}
|
|
6611
|
+
if (aggregate.field) {
|
|
6612
|
+
assertAllowedField(aggregate.field, "aggregates.field");
|
|
6613
|
+
}
|
|
6614
|
+
aggregateSortAllowlist.add(buildAggregateAlias(aggregate, index));
|
|
6615
|
+
}
|
|
6616
|
+
} else if (aggregateDefinitions.length > 0 || (payload.groupBy ?? []).length > 0) {
|
|
6617
|
+
throw new Error(
|
|
6618
|
+
`aggregates/groupBy payload requires queryMode='aggregate' on table '${tableName}'`
|
|
6619
|
+
);
|
|
6620
|
+
}
|
|
6621
|
+
if (options.enforceFieldAllowlist) {
|
|
6622
|
+
if (payload.fields) {
|
|
6623
|
+
for (const field of payload.fields) {
|
|
6624
|
+
assertAllowedField(field, "fields");
|
|
6625
|
+
}
|
|
6626
|
+
}
|
|
6627
|
+
if (payload.filter) {
|
|
6628
|
+
for (const field of Object.keys(payload.filter)) {
|
|
6629
|
+
assertAllowedField(field, "filter");
|
|
6630
|
+
}
|
|
6631
|
+
}
|
|
6632
|
+
if (payload.data) {
|
|
6633
|
+
const rows = resolveDataRows(payload.data);
|
|
6634
|
+
for (const row of rows) {
|
|
6635
|
+
for (const field of Object.keys(row)) {
|
|
6636
|
+
assertAllowedField(field, "data");
|
|
6637
|
+
}
|
|
6638
|
+
}
|
|
6639
|
+
}
|
|
6640
|
+
}
|
|
6641
|
+
if (payload.sort) {
|
|
6642
|
+
for (const field of Object.keys(payload.sort)) {
|
|
6643
|
+
if (resolvedMode === "aggregate" && aggregateSortAllowlist.has(field)) {
|
|
6644
|
+
continue;
|
|
6645
|
+
}
|
|
6646
|
+
assertAllowedField(field, "sort");
|
|
6647
|
+
}
|
|
6648
|
+
}
|
|
6649
|
+
if (payload.onConflict) {
|
|
6650
|
+
for (const conflictField of payload.onConflict.target ?? []) {
|
|
6651
|
+
assertAllowedField(conflictField, "onConflict.target");
|
|
6652
|
+
}
|
|
6653
|
+
for (const setField of Object.keys(payload.onConflict.action?.set ?? {})) {
|
|
6654
|
+
assertAllowedField(setField, "onConflict.action.set");
|
|
6655
|
+
}
|
|
6656
|
+
}
|
|
6657
|
+
if (payload.joins) {
|
|
6658
|
+
this.validateJoinPayload(registration.schema, payload.joins);
|
|
6659
|
+
}
|
|
6660
|
+
}
|
|
6661
|
+
validateJoinPayload(schema, joins) {
|
|
6662
|
+
for (const [joinTableName, joinDefinition] of Object.entries(joins)) {
|
|
6663
|
+
if (!schema.tables[joinTableName]) {
|
|
6664
|
+
throw new Error(`Invalid join table '${joinTableName}'. Table does not exist in schema.`);
|
|
6665
|
+
}
|
|
6666
|
+
const joinTable = schema.tables[joinTableName];
|
|
6667
|
+
for (const field of joinDefinition.fields ?? []) {
|
|
6668
|
+
if (!joinTable.fields[field]) {
|
|
6669
|
+
throw new Error(
|
|
6670
|
+
`Invalid join field '${field}' on joined table '${joinTableName}'`
|
|
6671
|
+
);
|
|
6672
|
+
}
|
|
6673
|
+
}
|
|
6674
|
+
if (joinDefinition.filter) {
|
|
6675
|
+
for (const filterField of Object.keys(joinDefinition.filter)) {
|
|
6676
|
+
if (!joinTable.fields[filterField]) {
|
|
6677
|
+
throw new Error(
|
|
6678
|
+
`Invalid join filter field '${filterField}' on joined table '${joinTableName}'`
|
|
6679
|
+
);
|
|
6680
|
+
}
|
|
6681
|
+
}
|
|
6682
|
+
}
|
|
6683
|
+
if (joinDefinition.joins) {
|
|
6684
|
+
this.validateJoinPayload(schema, joinDefinition.joins);
|
|
6685
|
+
}
|
|
6686
|
+
}
|
|
6687
|
+
}
|
|
6688
|
+
toCamelCase(rows) {
|
|
6689
|
+
return rows.map((row) => {
|
|
6690
|
+
const camelCasedRow = {};
|
|
6691
|
+
for (const [key, value] of Object.entries(row)) {
|
|
6692
|
+
camelCasedRow[(0, import_lodash_es.camelCase)(key)] = value;
|
|
6693
|
+
}
|
|
6694
|
+
return camelCasedRow;
|
|
6695
|
+
});
|
|
6696
|
+
}
|
|
6697
|
+
buildWhereClause(filter, params) {
|
|
6698
|
+
const conditions = [];
|
|
6699
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
6700
|
+
if (value !== void 0) {
|
|
6701
|
+
if (Array.isArray(value)) {
|
|
6702
|
+
const placeholders = value.map((entry) => {
|
|
6703
|
+
params.push(entry);
|
|
6704
|
+
return `$${params.length}`;
|
|
6705
|
+
}).join(", ");
|
|
6706
|
+
conditions.push(`${(0, import_lodash_es.snakeCase)(key)} IN (${placeholders})`);
|
|
6707
|
+
} else {
|
|
6708
|
+
params.push(value);
|
|
6709
|
+
conditions.push(`${(0, import_lodash_es.snakeCase)(key)} = $${params.length}`);
|
|
6710
|
+
}
|
|
6711
|
+
}
|
|
6712
|
+
}
|
|
6713
|
+
return conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
6714
|
+
}
|
|
6715
|
+
buildJoinClause(joins) {
|
|
6716
|
+
let joinSql = "";
|
|
6717
|
+
for (const [table, join] of Object.entries(joins)) {
|
|
6718
|
+
const alias = join.alias ? ` ${join.alias}` : "";
|
|
6719
|
+
joinSql += ` LEFT JOIN ${(0, import_lodash_es.snakeCase)(table)}${alias} ON ${join.on}`;
|
|
6720
|
+
if (join.joins) joinSql += " " + this.buildJoinClause(join.joins);
|
|
6721
|
+
}
|
|
6722
|
+
return joinSql;
|
|
6723
|
+
}
|
|
6724
|
+
async resolveNestedData(registration, data, tableName) {
|
|
6725
|
+
if (Array.isArray(data)) {
|
|
6726
|
+
return Promise.all(
|
|
6727
|
+
data.map((entry) => this.resolveNestedData(registration, entry, tableName))
|
|
6728
|
+
);
|
|
6729
|
+
}
|
|
6730
|
+
if (typeof data !== "object" || data === null) {
|
|
6731
|
+
return data;
|
|
6732
|
+
}
|
|
6733
|
+
const resolved = { ...data };
|
|
6734
|
+
for (const [key, value] of Object.entries(data)) {
|
|
6735
|
+
if (typeof value === "object" && value !== null && "subOperation" in value) {
|
|
6736
|
+
const subOperation = value;
|
|
6737
|
+
resolved[key] = await this.executeSubOperation(registration, subOperation);
|
|
6738
|
+
} else if (typeof value === "string" && ["increment", "decrement", "set"].includes(value)) {
|
|
6739
|
+
resolved[key] = { __effect: value };
|
|
6740
|
+
} else if (typeof value === "object" && value !== null) {
|
|
6741
|
+
resolved[key] = await this.resolveNestedData(registration, value, tableName);
|
|
6742
|
+
}
|
|
6743
|
+
}
|
|
6744
|
+
return resolved;
|
|
6745
|
+
}
|
|
6746
|
+
async executeSubOperation(registration, operation) {
|
|
6747
|
+
const targetTableName = operation.table;
|
|
6748
|
+
if (!registration.schema.tables[targetTableName]) {
|
|
6749
|
+
throw new Error(
|
|
6750
|
+
`Sub-operation table '${targetTableName}' does not exist in actor schema`
|
|
6452
6751
|
);
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6752
|
+
}
|
|
6753
|
+
const pool = this.getPoolOrThrow(registration);
|
|
6754
|
+
const safetyPolicy = this.resolveSafetyPolicy(registration);
|
|
6755
|
+
return this.executeWithTransaction(pool, true, async (client) => {
|
|
6756
|
+
if (operation.subOperation === "insert") {
|
|
6757
|
+
const resolvedData = await this.resolveNestedData(
|
|
6758
|
+
registration,
|
|
6759
|
+
operation.data,
|
|
6760
|
+
operation.table
|
|
6461
6761
|
);
|
|
6762
|
+
const row = ensurePlainObject(resolvedData, "sub-operation insert data");
|
|
6763
|
+
const keys = Object.keys(row);
|
|
6764
|
+
const params = Object.values(row);
|
|
6765
|
+
const sql2 = `INSERT INTO ${operation.table} (${keys.map((key) => (0, import_lodash_es.snakeCase)(key)).join(", ")}) VALUES (${params.map((_, index) => `$${index + 1}`).join(", ")}) ON CONFLICT DO NOTHING RETURNING ${operation.return ?? "*"}`;
|
|
6766
|
+
const result2 = await this.withTimeout(
|
|
6767
|
+
() => client.query(sql2, params),
|
|
6768
|
+
safetyPolicy.statementTimeoutMs,
|
|
6769
|
+
`Sub-operation insert timeout on table ${operation.table}`
|
|
6770
|
+
);
|
|
6771
|
+
const returnKey2 = operation.return ?? "uuid";
|
|
6772
|
+
if (result2.rows[0]?.[returnKey2] !== void 0) {
|
|
6773
|
+
return result2.rows[0][returnKey2];
|
|
6774
|
+
}
|
|
6775
|
+
return row[returnKey2] ?? row.uuid ?? {};
|
|
6462
6776
|
}
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6777
|
+
const queryParams = [];
|
|
6778
|
+
const whereClause = this.buildWhereClause(operation.filter || {}, queryParams);
|
|
6779
|
+
const sql = `SELECT ${(operation.fields ?? ["*"]).map((field) => field === "*" ? field : (0, import_lodash_es.snakeCase)(field)).join(", ")} FROM ${operation.table} ${whereClause} LIMIT 1`;
|
|
6780
|
+
const result = await this.withTimeout(
|
|
6781
|
+
() => client.query(sql, queryParams),
|
|
6782
|
+
safetyPolicy.statementTimeoutMs,
|
|
6783
|
+
`Sub-operation query timeout on table ${operation.table}`
|
|
6784
|
+
);
|
|
6785
|
+
const returnKey = operation.return ?? "uuid";
|
|
6786
|
+
return result.rows[0]?.[returnKey] ?? {};
|
|
6787
|
+
});
|
|
6472
6788
|
}
|
|
6473
6789
|
getInputSchema(op, tableName, table) {
|
|
6474
6790
|
const inputSchema = {
|
|
@@ -6487,66 +6803,43 @@ var DatabaseController = class _DatabaseController {
|
|
|
6487
6803
|
}
|
|
6488
6804
|
inputSchema.properties.transaction = getTransactionSchema();
|
|
6489
6805
|
inputSchema.properties.queryData.properties.transaction = inputSchema.properties.transaction;
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
inputSchema.properties.queryData.properties.limit = inputSchema.properties.limit;
|
|
6528
|
-
inputSchema.properties.offset = getQueryOffsetSchemaFromTable();
|
|
6529
|
-
inputSchema.properties.queryData.properties.offset = inputSchema.properties.offset;
|
|
6530
|
-
break;
|
|
6531
|
-
case "update":
|
|
6532
|
-
inputSchema.properties.filter = getQueryFilterSchemaFromTable(
|
|
6533
|
-
table,
|
|
6534
|
-
tableName
|
|
6535
|
-
);
|
|
6536
|
-
inputSchema.properties.queryData.properties.filter = inputSchema.properties.filter;
|
|
6537
|
-
inputSchema.properties.fields = getQueryFieldsSchemaFromTable(
|
|
6538
|
-
table,
|
|
6539
|
-
tableName
|
|
6540
|
-
);
|
|
6541
|
-
inputSchema.properties.queryData.properties.fields = inputSchema.properties.fields;
|
|
6542
|
-
break;
|
|
6543
|
-
case "delete":
|
|
6544
|
-
inputSchema.properties.filter = getQueryFilterSchemaFromTable(
|
|
6545
|
-
table,
|
|
6546
|
-
tableName
|
|
6547
|
-
);
|
|
6548
|
-
inputSchema.properties.queryData.properties.filter = inputSchema.properties.filter;
|
|
6549
|
-
break;
|
|
6806
|
+
if (op === "insert" || op === "update") {
|
|
6807
|
+
inputSchema.properties.data = getInsertDataSchemaFromTable(table, tableName);
|
|
6808
|
+
inputSchema.properties.queryData.properties.data = inputSchema.properties.data;
|
|
6809
|
+
}
|
|
6810
|
+
if (op === "insert") {
|
|
6811
|
+
inputSchema.properties.batch = getQueryBatchSchemaFromTable();
|
|
6812
|
+
inputSchema.properties.queryData.properties.batch = inputSchema.properties.batch;
|
|
6813
|
+
inputSchema.properties.onConflict = getQueryOnConflictSchemaFromTable(
|
|
6814
|
+
table,
|
|
6815
|
+
tableName
|
|
6816
|
+
);
|
|
6817
|
+
inputSchema.properties.queryData.properties.onConflict = inputSchema.properties.onConflict;
|
|
6818
|
+
}
|
|
6819
|
+
if (op === "query" || op === "update" || op === "delete") {
|
|
6820
|
+
inputSchema.properties.filter = getQueryFilterSchemaFromTable(table, tableName);
|
|
6821
|
+
inputSchema.properties.queryData.properties.filter = inputSchema.properties.filter;
|
|
6822
|
+
}
|
|
6823
|
+
if (op === "query") {
|
|
6824
|
+
inputSchema.properties.queryMode = getQueryModeSchema();
|
|
6825
|
+
inputSchema.properties.queryData.properties.queryMode = inputSchema.properties.queryMode;
|
|
6826
|
+
inputSchema.properties.fields = getQueryFieldsSchemaFromTable(table, tableName);
|
|
6827
|
+
inputSchema.properties.queryData.properties.fields = inputSchema.properties.fields;
|
|
6828
|
+
inputSchema.properties.joins = getQueryJoinsSchemaFromTable(table, tableName);
|
|
6829
|
+
inputSchema.properties.queryData.properties.joins = inputSchema.properties.joins;
|
|
6830
|
+
inputSchema.properties.sort = getQuerySortSchemaFromTable(table, tableName);
|
|
6831
|
+
inputSchema.properties.queryData.properties.sort = inputSchema.properties.sort;
|
|
6832
|
+
inputSchema.properties.aggregates = getQueryAggregatesSchemaFromTable(
|
|
6833
|
+
table,
|
|
6834
|
+
tableName
|
|
6835
|
+
);
|
|
6836
|
+
inputSchema.properties.queryData.properties.aggregates = inputSchema.properties.aggregates;
|
|
6837
|
+
inputSchema.properties.groupBy = getQueryGroupBySchemaFromTable(table, tableName);
|
|
6838
|
+
inputSchema.properties.queryData.properties.groupBy = inputSchema.properties.groupBy;
|
|
6839
|
+
inputSchema.properties.limit = getQueryLimitSchemaFromTable();
|
|
6840
|
+
inputSchema.properties.queryData.properties.limit = inputSchema.properties.limit;
|
|
6841
|
+
inputSchema.properties.offset = getQueryOffsetSchemaFromTable();
|
|
6842
|
+
inputSchema.properties.queryData.properties.offset = inputSchema.properties.offset;
|
|
6550
6843
|
}
|
|
6551
6844
|
return inputSchema;
|
|
6552
6845
|
}
|
|
@@ -6556,13 +6849,13 @@ function getInsertDataSchemaFromTable(table, tableName) {
|
|
|
6556
6849
|
type: "object",
|
|
6557
6850
|
properties: {
|
|
6558
6851
|
...Object.fromEntries(
|
|
6559
|
-
Object.entries(table.fields).map((field) => {
|
|
6852
|
+
Object.entries(table.fields).map(([fieldName, field]) => {
|
|
6560
6853
|
return [
|
|
6561
|
-
|
|
6854
|
+
fieldName,
|
|
6562
6855
|
{
|
|
6563
6856
|
value: {
|
|
6564
|
-
type: tableFieldTypeToSchemaType(field
|
|
6565
|
-
description: `Inferred from field '${
|
|
6857
|
+
type: tableFieldTypeToSchemaType(field.type),
|
|
6858
|
+
description: `Inferred from field '${fieldName}' of type [${field.type}] on table ${tableName}.`
|
|
6566
6859
|
},
|
|
6567
6860
|
effect: {
|
|
6568
6861
|
type: "string",
|
|
@@ -6611,7 +6904,7 @@ function getInsertDataSchemaFromTable(table, tableName) {
|
|
|
6611
6904
|
})
|
|
6612
6905
|
)
|
|
6613
6906
|
},
|
|
6614
|
-
required: Object.entries(table.fields).filter((field) => field
|
|
6907
|
+
required: Object.entries(table.fields).filter(([, field]) => field.required || field.primary).map(([fieldName]) => fieldName),
|
|
6615
6908
|
strict: true
|
|
6616
6909
|
};
|
|
6617
6910
|
return {
|
|
@@ -6627,18 +6920,18 @@ function getQueryFilterSchemaFromTable(table, tableName) {
|
|
|
6627
6920
|
type: "object",
|
|
6628
6921
|
properties: {
|
|
6629
6922
|
...Object.fromEntries(
|
|
6630
|
-
Object.entries(table.fields).map((field) => {
|
|
6923
|
+
Object.entries(table.fields).map(([fieldName, field]) => {
|
|
6631
6924
|
return [
|
|
6632
|
-
|
|
6925
|
+
fieldName,
|
|
6633
6926
|
{
|
|
6634
6927
|
value: {
|
|
6635
|
-
type: tableFieldTypeToSchemaType(field
|
|
6636
|
-
description: `Inferred from field '${
|
|
6928
|
+
type: tableFieldTypeToSchemaType(field.type),
|
|
6929
|
+
description: `Inferred from field '${fieldName}' of type [${field.type}] on table ${tableName}.`
|
|
6637
6930
|
},
|
|
6638
6931
|
in: {
|
|
6639
6932
|
type: "array",
|
|
6640
6933
|
items: {
|
|
6641
|
-
type: tableFieldTypeToSchemaType(field
|
|
6934
|
+
type: tableFieldTypeToSchemaType(field.type)
|
|
6642
6935
|
}
|
|
6643
6936
|
}
|
|
6644
6937
|
}
|
|
@@ -6647,7 +6940,7 @@ function getQueryFilterSchemaFromTable(table, tableName) {
|
|
|
6647
6940
|
)
|
|
6648
6941
|
},
|
|
6649
6942
|
strict: true,
|
|
6650
|
-
description: `Inferred from table '${tableName}' on
|
|
6943
|
+
description: `Inferred from table '${tableName}' on postgres actor table contract.`
|
|
6651
6944
|
};
|
|
6652
6945
|
}
|
|
6653
6946
|
function getQueryFieldsSchemaFromTable(table, tableName) {
|
|
@@ -6659,106 +6952,100 @@ function getQueryFieldsSchemaFromTable(table, tableName) {
|
|
|
6659
6952
|
oneOf: Object.keys(table.fields)
|
|
6660
6953
|
}
|
|
6661
6954
|
},
|
|
6662
|
-
description: `Inferred from table '${tableName}'
|
|
6955
|
+
description: `Inferred field projection from table '${tableName}'.`
|
|
6663
6956
|
};
|
|
6664
6957
|
}
|
|
6665
|
-
function
|
|
6958
|
+
function getQueryModeSchema() {
|
|
6666
6959
|
return {
|
|
6667
|
-
type: "
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
},
|
|
6701
|
-
required: ["on", "fields"],
|
|
6702
|
-
strict: true
|
|
6703
|
-
}
|
|
6704
|
-
];
|
|
6705
|
-
})
|
|
6706
|
-
)
|
|
6960
|
+
type: "string",
|
|
6961
|
+
constraints: {
|
|
6962
|
+
oneOf: ["rows", "count", "exists", "one", "aggregate"]
|
|
6963
|
+
}
|
|
6964
|
+
};
|
|
6965
|
+
}
|
|
6966
|
+
function getQueryAggregatesSchemaFromTable(table, tableName) {
|
|
6967
|
+
return {
|
|
6968
|
+
type: "array",
|
|
6969
|
+
items: {
|
|
6970
|
+
type: "object",
|
|
6971
|
+
properties: {
|
|
6972
|
+
fn: {
|
|
6973
|
+
type: "string",
|
|
6974
|
+
constraints: {
|
|
6975
|
+
oneOf: ["count", "sum", "avg", "min", "max"]
|
|
6976
|
+
}
|
|
6977
|
+
},
|
|
6978
|
+
field: {
|
|
6979
|
+
type: "string",
|
|
6980
|
+
constraints: {
|
|
6981
|
+
oneOf: Object.keys(table.fields)
|
|
6982
|
+
}
|
|
6983
|
+
},
|
|
6984
|
+
as: {
|
|
6985
|
+
type: "string"
|
|
6986
|
+
},
|
|
6987
|
+
distinct: {
|
|
6988
|
+
type: "boolean"
|
|
6989
|
+
}
|
|
6990
|
+
},
|
|
6991
|
+
required: ["fn"],
|
|
6992
|
+
strict: true
|
|
6707
6993
|
},
|
|
6708
|
-
|
|
6709
|
-
description: `Inferred from table '${tableName}' on database service ${CadenzaService.serviceRegistry?.serviceName ?? "unknown-service"}.`
|
|
6994
|
+
description: `Aggregate definitions inferred from table '${tableName}'.`
|
|
6710
6995
|
};
|
|
6711
6996
|
}
|
|
6712
|
-
function
|
|
6997
|
+
function getQueryGroupBySchemaFromTable(table, tableName) {
|
|
6713
6998
|
return {
|
|
6714
|
-
type: "
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
{
|
|
6721
|
-
type: "string",
|
|
6722
|
-
constraints: {
|
|
6723
|
-
oneOf: ["asc", "desc"]
|
|
6724
|
-
}
|
|
6725
|
-
}
|
|
6726
|
-
];
|
|
6727
|
-
})
|
|
6728
|
-
)
|
|
6999
|
+
type: "array",
|
|
7000
|
+
items: {
|
|
7001
|
+
type: "string",
|
|
7002
|
+
constraints: {
|
|
7003
|
+
oneOf: Object.keys(table.fields)
|
|
7004
|
+
}
|
|
6729
7005
|
},
|
|
6730
|
-
|
|
6731
|
-
|
|
7006
|
+
description: `Group by fields inferred from table '${tableName}'.`
|
|
7007
|
+
};
|
|
7008
|
+
}
|
|
7009
|
+
function getQueryJoinsSchemaFromTable(_table, tableName) {
|
|
7010
|
+
return {
|
|
7011
|
+
type: "object",
|
|
7012
|
+
description: `Join definitions for table '${tableName}'.`
|
|
7013
|
+
};
|
|
7014
|
+
}
|
|
7015
|
+
function getQuerySortSchemaFromTable(_table, tableName) {
|
|
7016
|
+
return {
|
|
7017
|
+
type: "object",
|
|
7018
|
+
strict: false,
|
|
7019
|
+
description: `Sort definition for table '${tableName}'. Keys are validated at runtime against allowlists and aggregate aliases.`
|
|
6732
7020
|
};
|
|
6733
7021
|
}
|
|
6734
7022
|
function getQueryLimitSchemaFromTable() {
|
|
6735
7023
|
return {
|
|
6736
7024
|
type: "number",
|
|
6737
7025
|
constraints: {
|
|
6738
|
-
min:
|
|
6739
|
-
|
|
6740
|
-
|
|
7026
|
+
min: 0,
|
|
7027
|
+
max: 1e3
|
|
7028
|
+
}
|
|
6741
7029
|
};
|
|
6742
7030
|
}
|
|
6743
7031
|
function getQueryOffsetSchemaFromTable() {
|
|
6744
7032
|
return {
|
|
6745
7033
|
type: "number",
|
|
6746
7034
|
constraints: {
|
|
6747
|
-
min: 0
|
|
6748
|
-
|
|
6749
|
-
|
|
7035
|
+
min: 0,
|
|
7036
|
+
max: 1e6
|
|
7037
|
+
}
|
|
6750
7038
|
};
|
|
6751
7039
|
}
|
|
6752
|
-
function
|
|
7040
|
+
function getQueryBatchSchemaFromTable() {
|
|
6753
7041
|
return {
|
|
6754
|
-
type: "boolean"
|
|
6755
|
-
description: "Whether to run the query in a transaction"
|
|
7042
|
+
type: "boolean"
|
|
6756
7043
|
};
|
|
6757
7044
|
}
|
|
6758
|
-
function
|
|
7045
|
+
function getTransactionSchema() {
|
|
6759
7046
|
return {
|
|
6760
7047
|
type: "boolean",
|
|
6761
|
-
description: "
|
|
7048
|
+
description: "Execute the operation in a transaction."
|
|
6762
7049
|
};
|
|
6763
7050
|
}
|
|
6764
7051
|
function getQueryOnConflictSchemaFromTable(table, tableName) {
|
|
@@ -6784,55 +7071,43 @@ function getQueryOnConflictSchemaFromTable(table, tableName) {
|
|
|
6784
7071
|
}
|
|
6785
7072
|
},
|
|
6786
7073
|
set: {
|
|
6787
|
-
type: "object"
|
|
6788
|
-
properties: {
|
|
6789
|
-
...Object.fromEntries(
|
|
6790
|
-
Object.entries(table.fields).map((field) => {
|
|
6791
|
-
return [
|
|
6792
|
-
field[0],
|
|
6793
|
-
{
|
|
6794
|
-
type: tableFieldTypeToSchemaType(field[1].type),
|
|
6795
|
-
description: `Inferred from field '${field[0]}' of type [${field[1].type}] on table ${tableName}.`
|
|
6796
|
-
}
|
|
6797
|
-
];
|
|
6798
|
-
})
|
|
6799
|
-
)
|
|
6800
|
-
}
|
|
7074
|
+
type: "object"
|
|
6801
7075
|
},
|
|
6802
7076
|
where: {
|
|
6803
7077
|
type: "string"
|
|
6804
7078
|
}
|
|
6805
|
-
}
|
|
6806
|
-
required: ["do"]
|
|
7079
|
+
}
|
|
6807
7080
|
}
|
|
6808
7081
|
},
|
|
6809
|
-
|
|
6810
|
-
|
|
7082
|
+
strict: true,
|
|
7083
|
+
description: `Conflict strategy for inserts on table '${tableName}'.`
|
|
6811
7084
|
};
|
|
6812
7085
|
}
|
|
6813
7086
|
function tableFieldTypeToSchemaType(type) {
|
|
6814
7087
|
switch (type) {
|
|
6815
7088
|
case "varchar":
|
|
6816
7089
|
case "text":
|
|
6817
|
-
case "jsonb":
|
|
6818
7090
|
case "uuid":
|
|
7091
|
+
case "timestamp":
|
|
6819
7092
|
case "date":
|
|
6820
7093
|
case "geo_point":
|
|
6821
|
-
case "bytea":
|
|
6822
7094
|
return "string";
|
|
6823
7095
|
case "int":
|
|
6824
7096
|
case "bigint":
|
|
6825
7097
|
case "decimal":
|
|
6826
|
-
case "timestamp":
|
|
6827
7098
|
return "number";
|
|
6828
7099
|
case "boolean":
|
|
6829
7100
|
return "boolean";
|
|
6830
7101
|
case "array":
|
|
6831
7102
|
return "array";
|
|
6832
7103
|
case "object":
|
|
7104
|
+
case "jsonb":
|
|
6833
7105
|
return "object";
|
|
7106
|
+
case "bytea":
|
|
7107
|
+
return "string";
|
|
7108
|
+
default:
|
|
7109
|
+
return "any";
|
|
6834
7110
|
}
|
|
6835
|
-
return "any";
|
|
6836
7111
|
}
|
|
6837
7112
|
|
|
6838
7113
|
// src/Cadenza.ts
|
|
@@ -8408,14 +8683,61 @@ var CadenzaService = class {
|
|
|
8408
8683
|
this.createCadenzaService(serviceName, description, options);
|
|
8409
8684
|
}
|
|
8410
8685
|
/**
|
|
8411
|
-
* Creates and initializes a
|
|
8412
|
-
* This
|
|
8686
|
+
* Creates and initializes a specialized PostgresActor.
|
|
8687
|
+
* This is actor-only and does not create or register a network service.
|
|
8413
8688
|
*
|
|
8414
|
-
* @param {string} name -
|
|
8415
|
-
* @param {DatabaseSchemaDefinition} schema -
|
|
8416
|
-
* @param {string} [description=""] -
|
|
8417
|
-
* @param {ServerOptions & DatabaseOptions} [options={}] -
|
|
8418
|
-
* @return {void}
|
|
8689
|
+
* @param {string} name - Logical PostgresActor name.
|
|
8690
|
+
* @param {DatabaseSchemaDefinition} schema - Database schema definition.
|
|
8691
|
+
* @param {string} [description=""] - Optional human-readable actor description.
|
|
8692
|
+
* @param {ServerOptions & DatabaseOptions} [options={}] - Actor/database runtime options.
|
|
8693
|
+
* @return {void}
|
|
8694
|
+
*/
|
|
8695
|
+
static createPostgresActor(name, schema, description = "", options = {}) {
|
|
8696
|
+
if (isBrowser) {
|
|
8697
|
+
console.warn(
|
|
8698
|
+
"PostgresActor creation is not supported in the browser."
|
|
8699
|
+
);
|
|
8700
|
+
return;
|
|
8701
|
+
}
|
|
8702
|
+
this.bootstrap();
|
|
8703
|
+
this.validateName(name);
|
|
8704
|
+
const databaseController = DatabaseController.instance;
|
|
8705
|
+
const normalizedOptions = this.normalizePostgresActorOptions(name, options);
|
|
8706
|
+
const registration = databaseController.createPostgresActor(
|
|
8707
|
+
name,
|
|
8708
|
+
schema,
|
|
8709
|
+
description,
|
|
8710
|
+
normalizedOptions
|
|
8711
|
+
);
|
|
8712
|
+
console.log("Creating PostgresActor", {
|
|
8713
|
+
name,
|
|
8714
|
+
actorName: registration.actorName,
|
|
8715
|
+
ownerServiceName: normalizedOptions.ownerServiceName ?? null,
|
|
8716
|
+
options: normalizedOptions
|
|
8717
|
+
});
|
|
8718
|
+
databaseController.requestPostgresActorSetup(registration, {
|
|
8719
|
+
actorName: registration.actorName,
|
|
8720
|
+
actorToken: registration.actorToken,
|
|
8721
|
+
databaseName: normalizedOptions.databaseName,
|
|
8722
|
+
ownerServiceName: normalizedOptions.ownerServiceName ?? null
|
|
8723
|
+
});
|
|
8724
|
+
}
|
|
8725
|
+
/**
|
|
8726
|
+
* Creates a meta PostgresActor.
|
|
8727
|
+
*
|
|
8728
|
+
* @param {string} name - Logical PostgresActor name.
|
|
8729
|
+
* @param {DatabaseSchemaDefinition} schema - Database schema definition.
|
|
8730
|
+
* @param {string} [description=""] - Optional description.
|
|
8731
|
+
* @param {ServerOptions & DatabaseOptions} [options={}] - Optional actor/database options.
|
|
8732
|
+
* @return {void}
|
|
8733
|
+
*/
|
|
8734
|
+
static createMetaPostgresActor(name, schema, description = "", options = {}) {
|
|
8735
|
+
this.bootstrap();
|
|
8736
|
+
options.isMeta = true;
|
|
8737
|
+
this.createPostgresActor(name, schema, description, options);
|
|
8738
|
+
}
|
|
8739
|
+
/**
|
|
8740
|
+
* Creates a dedicated database service by composing a PostgresActor and a Cadenza service.
|
|
8419
8741
|
*/
|
|
8420
8742
|
static createDatabaseService(name, schema, description = "", options = {}) {
|
|
8421
8743
|
if (isBrowser) {
|
|
@@ -8426,9 +8748,102 @@ var CadenzaService = class {
|
|
|
8426
8748
|
}
|
|
8427
8749
|
if (this.serviceCreated) return;
|
|
8428
8750
|
this.bootstrap();
|
|
8429
|
-
this.
|
|
8430
|
-
|
|
8431
|
-
|
|
8751
|
+
this.validateName(name);
|
|
8752
|
+
this.validateServiceName(name);
|
|
8753
|
+
const databaseController = DatabaseController.instance;
|
|
8754
|
+
const actorOptions = this.normalizePostgresActorOptions(name, {
|
|
8755
|
+
...options,
|
|
8756
|
+
ownerServiceName: options.ownerServiceName ?? name
|
|
8757
|
+
});
|
|
8758
|
+
const serviceOptions = this.normalizeDatabaseServiceOptions(name, {
|
|
8759
|
+
...options,
|
|
8760
|
+
ownerServiceName: actorOptions.ownerServiceName
|
|
8761
|
+
});
|
|
8762
|
+
const registration = databaseController.createPostgresActor(
|
|
8763
|
+
name,
|
|
8764
|
+
schema,
|
|
8765
|
+
description,
|
|
8766
|
+
actorOptions
|
|
8767
|
+
);
|
|
8768
|
+
this.registerDatabaseServiceBridgeTask(
|
|
8769
|
+
name,
|
|
8770
|
+
description,
|
|
8771
|
+
schema,
|
|
8772
|
+
Boolean(serviceOptions.isMeta),
|
|
8773
|
+
registration.actorName
|
|
8774
|
+
);
|
|
8775
|
+
const createServiceTaskName = `Create database service ${name} after ${registration.actorName} setup`;
|
|
8776
|
+
if (!this.get(createServiceTaskName)) {
|
|
8777
|
+
this.createMetaTask(
|
|
8778
|
+
createServiceTaskName,
|
|
8779
|
+
() => {
|
|
8780
|
+
this.createCadenzaService(name, description, serviceOptions);
|
|
8781
|
+
return true;
|
|
8782
|
+
},
|
|
8783
|
+
"Creates the networked database service after PostgresActor setup completes."
|
|
8784
|
+
).doOn(registration.setupDoneSignal);
|
|
8785
|
+
}
|
|
8786
|
+
const setupFailureTaskName = `Handle database service ${name} bootstrap failure`;
|
|
8787
|
+
if (!this.get(setupFailureTaskName)) {
|
|
8788
|
+
this.createMetaTask(
|
|
8789
|
+
setupFailureTaskName,
|
|
8790
|
+
(ctx) => {
|
|
8791
|
+
this.log(
|
|
8792
|
+
"Database service bootstrap failed before service creation.",
|
|
8793
|
+
{
|
|
8794
|
+
serviceName: name,
|
|
8795
|
+
actorName: registration.actorName,
|
|
8796
|
+
databaseName: registration.databaseName,
|
|
8797
|
+
error: ctx.__error
|
|
8798
|
+
},
|
|
8799
|
+
"error"
|
|
8800
|
+
);
|
|
8801
|
+
return true;
|
|
8802
|
+
},
|
|
8803
|
+
"Logs PostgresActor setup failures for database service bootstrap."
|
|
8804
|
+
).doOn(registration.setupFailedSignal);
|
|
8805
|
+
}
|
|
8806
|
+
console.log("Creating database service wrapper", {
|
|
8807
|
+
serviceName: name,
|
|
8808
|
+
actorName: registration.actorName,
|
|
8809
|
+
actorOptions,
|
|
8810
|
+
serviceOptions
|
|
8811
|
+
});
|
|
8812
|
+
databaseController.requestPostgresActorSetup(registration, {
|
|
8813
|
+
actorName: registration.actorName,
|
|
8814
|
+
actorToken: registration.actorToken,
|
|
8815
|
+
databaseName: actorOptions.databaseName,
|
|
8816
|
+
ownerServiceName: actorOptions.ownerServiceName ?? name
|
|
8817
|
+
});
|
|
8818
|
+
}
|
|
8819
|
+
/**
|
|
8820
|
+
* Creates a meta database service with the specified configuration.
|
|
8821
|
+
*
|
|
8822
|
+
* @param {string} name - The name of the database service to be created.
|
|
8823
|
+
* @param {DatabaseSchemaDefinition} schema - The schema definition for the database.
|
|
8824
|
+
* @param {string} [description=""] - An optional description of the database service.
|
|
8825
|
+
* @param {ServerOptions & DatabaseOptions} [options={}] - Optional server and database configuration options. The `isMeta` flag will be automatically set to true.
|
|
8826
|
+
* @return {void} - This method does not return a value.
|
|
8827
|
+
*/
|
|
8828
|
+
static createMetaDatabaseService(name, schema, description = "", options = {}) {
|
|
8829
|
+
this.createDatabaseService(name, schema, description, {
|
|
8830
|
+
...options,
|
|
8831
|
+
isMeta: true
|
|
8832
|
+
});
|
|
8833
|
+
}
|
|
8834
|
+
static normalizePostgresActorOptions(name, options = {}) {
|
|
8835
|
+
return {
|
|
8836
|
+
isMeta: false,
|
|
8837
|
+
retryCount: 3,
|
|
8838
|
+
databaseType: "postgres",
|
|
8839
|
+
databaseName: (0, import_lodash_es2.snakeCase)(name),
|
|
8840
|
+
poolSize: parseInt(process.env.DATABASE_POOL_SIZE ?? "10"),
|
|
8841
|
+
ownerServiceName: options.ownerServiceName ?? this.serviceRegistry?.serviceName ?? null,
|
|
8842
|
+
...options
|
|
8843
|
+
};
|
|
8844
|
+
}
|
|
8845
|
+
static normalizeDatabaseServiceOptions(name, options = {}) {
|
|
8846
|
+
return {
|
|
8432
8847
|
loadBalance: true,
|
|
8433
8848
|
useSocket: true,
|
|
8434
8849
|
displayName: void 0,
|
|
@@ -8446,45 +8861,38 @@ var CadenzaService = class {
|
|
|
8446
8861
|
databaseName: (0, import_lodash_es2.snakeCase)(name),
|
|
8447
8862
|
poolSize: parseInt(process.env.DATABASE_POOL_SIZE ?? "10"),
|
|
8448
8863
|
isDatabase: true,
|
|
8864
|
+
ownerServiceName: options.ownerServiceName ?? name,
|
|
8449
8865
|
...options
|
|
8450
8866
|
};
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
8456
|
-
}
|
|
8457
|
-
this.createMetaTask(
|
|
8458
|
-
|
|
8867
|
+
}
|
|
8868
|
+
static registerDatabaseServiceBridgeTask(serviceName, description, schema, isMeta, actorName) {
|
|
8869
|
+
const taskName = `Insert database service bridge ${serviceName}`;
|
|
8870
|
+
if (this.get(taskName)) {
|
|
8871
|
+
return;
|
|
8872
|
+
}
|
|
8873
|
+
this.createMetaTask(
|
|
8874
|
+
taskName,
|
|
8875
|
+
(ctx, emit) => {
|
|
8876
|
+
if (ctx.__serviceName && ctx.__serviceName !== serviceName) {
|
|
8877
|
+
return false;
|
|
8878
|
+
}
|
|
8459
8879
|
emit("global.meta.created_database_service", {
|
|
8460
8880
|
data: {
|
|
8461
|
-
service_name:
|
|
8881
|
+
service_name: serviceName,
|
|
8462
8882
|
description,
|
|
8463
8883
|
schema,
|
|
8464
|
-
is_meta:
|
|
8884
|
+
is_meta: isMeta
|
|
8465
8885
|
}
|
|
8466
8886
|
});
|
|
8467
8887
|
this.log("Database service created", {
|
|
8468
|
-
name,
|
|
8469
|
-
isMeta
|
|
8888
|
+
name: serviceName,
|
|
8889
|
+
isMeta,
|
|
8890
|
+
actorName
|
|
8470
8891
|
});
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
/**
|
|
8476
|
-
* Creates a meta database service with the specified configuration.
|
|
8477
|
-
*
|
|
8478
|
-
* @param {string} name - The name of the database service to be created.
|
|
8479
|
-
* @param {DatabaseSchemaDefinition} schema - The schema definition for the database.
|
|
8480
|
-
* @param {string} [description=""] - An optional description of the database service.
|
|
8481
|
-
* @param {ServerOptions & DatabaseOptions} [options={}] - Optional server and database configuration options. The `isMeta` flag will be automatically set to true.
|
|
8482
|
-
* @return {void} - This method does not return a value.
|
|
8483
|
-
*/
|
|
8484
|
-
static createMetaDatabaseService(name, schema, description = "", options = {}) {
|
|
8485
|
-
this.bootstrap();
|
|
8486
|
-
options.isMeta = true;
|
|
8487
|
-
this.createDatabaseService(name, schema, description, options);
|
|
8892
|
+
return true;
|
|
8893
|
+
},
|
|
8894
|
+
"Bridges database service creation into the global metadata signal."
|
|
8895
|
+
).doOn("meta.service_registry.service_inserted");
|
|
8488
8896
|
}
|
|
8489
8897
|
static createActor(spec, options = {}) {
|
|
8490
8898
|
this.bootstrap();
|
|
@@ -8837,14 +9245,14 @@ var CadenzaService = class {
|
|
|
8837
9245
|
return import_core4.default.createEphemeralTask(name, func, description, options);
|
|
8838
9246
|
}
|
|
8839
9247
|
/**
|
|
8840
|
-
* Creates an ephemeral meta
|
|
9248
|
+
* Creates an ephemeral meta-task with the specified name, function, description, and options.
|
|
8841
9249
|
* See {@link createEphemeralTask} and {@link createMetaTask} for more details.
|
|
8842
9250
|
*
|
|
8843
9251
|
* @param {string} name - The name of the task to be created.
|
|
8844
9252
|
* @param {TaskFunction} func - The function to be executed as part of the task.
|
|
8845
9253
|
* @param {string} [description] - An optional description of the task.
|
|
8846
9254
|
* @param {TaskOptions & EphemeralTaskOptions} [options={}] - Additional options for configuring the task.
|
|
8847
|
-
* @return {EphemeralTask} The created ephemeral meta
|
|
9255
|
+
* @return {EphemeralTask} The created ephemeral meta-task.
|
|
8848
9256
|
*/
|
|
8849
9257
|
static createEphemeralMetaTask(name, func, description, options = {}) {
|
|
8850
9258
|
this.bootstrap();
|