@cadenza.io/service 2.11.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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
- "serviceInstance",
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 = "No routine or task defined.";
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 URL = `${protocol}://${serviceAddress}:${port}`;
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
- URL
2890
+ URL2
2859
2891
  );
2860
2892
  fetchDiagnostics.destroyed = false;
2861
2893
  fetchDiagnostics.updatedAt = Date.now();
2862
- if (CadenzaService.get(`Send Handshake to ${URL}`)) {
2863
- console.error("Fetch client already exists", URL);
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 ${URL}`,
2899
+ `Send Handshake to ${URL2}`,
2868
2900
  async (ctx2, emit) => {
2869
2901
  try {
2870
2902
  const response = await this.fetchDataWithTimeout(
2871
- `${URL}/handshake`,
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, URL, error);
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, URL, e);
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 ${URL}`,
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
- `${URL}/delegation`,
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
- URL,
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, URL, e);
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 ${URL}`,
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
- `${URL}/signal`,
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
- URL,
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, URL, e);
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 ${URL}`,
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
- `${URL}/status`,
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
- URL,
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, URL, e);
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 errorMessage = input.error !== void 0 ? this.getErrorMessage(input.error) : void 0;
3488
- if (errorMessage) {
3489
- base.lastError = errorMessage;
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: errorMessage
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 = (errorMessage, fallbackError) => {
4027
+ const resolveWithError = (errorMessage2, fallbackError) => {
3996
4028
  settle({
3997
4029
  ...data,
3998
4030
  errored: true,
3999
- __error: errorMessage,
4000
- error: fallbackError instanceof Error ? fallbackError.message : errorMessage,
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 errorMessage = result?.__error ?? result?.error ?? "Socket handshake failed";
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: errorMessage
4342
+ lastHandshakeError: errorMessage2
4311
4343
  },
4312
- errorMessage
4344
+ errorMessage2
4313
4345
  );
4314
4346
  applySessionOperation("handshake", {
4315
4347
  connected: false,
4316
4348
  handshake: false,
4317
- lastHandshakeError: errorMessage
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 errorMessage = resultContext?.__error ?? resultContext?.error ?? "Socket delegation failed";
4397
+ const errorMessage2 = resultContext?.__error ?? resultContext?.error ?? "Socket delegation failed";
4366
4398
  upsertDiagnostics(
4367
4399
  {
4368
- lastHandshakeError: String(errorMessage)
4400
+ lastHandshakeError: String(errorMessage2)
4369
4401
  },
4370
- errorMessage
4402
+ errorMessage2
4371
4403
  );
4372
4404
  applySessionOperation("delegate", {
4373
- lastHandshakeError: String(errorMessage)
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 resolveTableQueryIntents(serviceName, tableName, table, defaultInputSchema) {
5209
- const resolvedServiceName = serviceName ?? "unknown-service";
5210
- const defaultIntentName = `query-${resolvedServiceName}-${tableName}`;
5211
- const defaultDescription = `Perform a query operation on the ${tableName} table`;
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 validateIntentName(intentName) {
5248
+ if (!intentName || typeof intentName !== "string") {
5249
+ throw new Error("Intent name must be a non-empty string");
5250
+ }
5251
+ if (intentName.length > 100) {
5252
+ throw new Error(`Intent name must be <= 100 characters: ${intentName}`);
5253
+ }
5254
+ if (intentName.includes(" ") || intentName.includes(".") || intentName.includes("\\")) {
5255
+ throw new Error(
5256
+ `Intent name cannot contain spaces, dots or backslashes: ${intentName}`
5257
+ );
5258
+ }
5259
+ }
5260
+ function defaultOperationIntentDescription(operation, tableName) {
5261
+ return `Perform a ${operation} operation on the ${tableName} table`;
5262
+ }
5263
+ function readCustomIntentConfig(customIntent) {
5264
+ if (typeof customIntent === "string") {
5265
+ return {
5266
+ intent: customIntent
5267
+ };
5268
+ }
5269
+ return {
5270
+ intent: customIntent.intent,
5271
+ description: customIntent.description,
5272
+ input: customIntent.input
5273
+ };
5274
+ }
5275
+ function resolveTableOperationIntents(actorName, tableName, table, operation, defaultInputSchema) {
5276
+ const actorToken = normalizeIntentToken(actorName);
5277
+ const defaultIntentName = `${operation}-pg-${actorToken}-${tableName}`;
5278
+ validateIntentName(defaultIntentName);
5212
5279
  const intents = [
5213
5280
  {
5214
5281
  name: defaultIntentName,
5215
- description: defaultDescription,
5282
+ description: defaultOperationIntentDescription(operation, tableName),
5216
5283
  input: defaultInputSchema
5217
5284
  }
5218
5285
  ];
5219
- const warnings = [];
5220
- const names = /* @__PURE__ */ new Set([defaultIntentName]);
5221
- for (const customIntent of table.customIntents?.query ?? []) {
5222
- const name = typeof customIntent === "string" ? customIntent.trim() : customIntent.intent?.trim();
5223
- if (!name) {
5224
- warnings.push(
5225
- `Skipped empty custom query intent for table '${tableName}'.`
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.`
5286
+ const seenNames = /* @__PURE__ */ new Set([defaultIntentName]);
5287
+ const customIntentList = table.customIntents?.[operation] ?? [];
5288
+ for (const rawCustomIntent of customIntentList) {
5289
+ const customIntent = readCustomIntentConfig(rawCustomIntent);
5290
+ const intentName = String(customIntent.intent ?? "").trim();
5291
+ if (!intentName) {
5292
+ throw new Error(
5293
+ `Invalid custom ${operation} intent on table '${tableName}': intent must be a non-empty string`
5238
5294
  );
5239
- continue;
5240
5295
  }
5241
- if (names.has(name)) {
5242
- warnings.push(
5243
- `Skipped duplicate custom query intent '${name}' for table '${tableName}'.`
5296
+ validateIntentName(intentName);
5297
+ if (seenNames.has(intentName)) {
5298
+ throw new Error(
5299
+ `Duplicate ${operation} intent '${intentName}' on table '${tableName}'`
5244
5300
  );
5245
- continue;
5246
5301
  }
5247
- names.add(name);
5302
+ seenNames.add(intentName);
5248
5303
  intents.push({
5249
- name,
5250
- description: typeof customIntent === "string" ? `Perform a query operation on the ${tableName} table` : customIntent.description ?? defaultDescription,
5251
- input: typeof customIntent === "string" ? defaultInputSchema : customIntent.input ?? defaultInputSchema
5304
+ name: intentName,
5305
+ description: customIntent.description ?? defaultOperationIntentDescription(operation, tableName),
5306
+ input: customIntent.input ?? defaultInputSchema
5252
5307
  });
5253
5308
  }
5254
- return { intents, warnings };
5309
+ return { intents };
5310
+ }
5311
+ function ensurePlainObject(value, label) {
5312
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
5313
+ throw new Error(`${label} must be an object`);
5314
+ }
5315
+ return value;
5316
+ }
5317
+ function sleep(timeoutMs) {
5318
+ return new Promise((resolve) => {
5319
+ setTimeout(resolve, timeoutMs);
5320
+ });
5321
+ }
5322
+ function normalizePositiveInteger(value, fallback, min = 1) {
5323
+ if (typeof value !== "number" || !Number.isFinite(value)) {
5324
+ return fallback;
5325
+ }
5326
+ const normalized = Math.trunc(value);
5327
+ if (normalized < min) {
5328
+ return fallback;
5329
+ }
5330
+ return normalized;
5331
+ }
5332
+ function errorMessage(error) {
5333
+ if (error instanceof Error) {
5334
+ return error.message;
5335
+ }
5336
+ return String(error);
5337
+ }
5338
+ function isTransientDatabaseError(error) {
5339
+ if (!error || typeof error !== "object") {
5340
+ return false;
5341
+ }
5342
+ const dbError = error;
5343
+ const code = String(dbError.code ?? "");
5344
+ if (["40001", "40P01", "57P03", "53300", "08006", "08001"].includes(code)) {
5345
+ return true;
5346
+ }
5347
+ const message = String(dbError.message ?? "").toLowerCase();
5348
+ return message.includes("timeout") || message.includes("terminating connection") || message.includes("connection reset");
5349
+ }
5350
+ function isSqlIdentifier(value) {
5351
+ return /^[a-z_][a-z0-9_]*$/.test(value);
5352
+ }
5353
+ function toSafeSqlIdentifier(value, fallback) {
5354
+ const normalized = (0, import_lodash_es.snakeCase)(value || "").replace(/[^a-z0-9_]/g, "_");
5355
+ const candidate = normalized || fallback;
5356
+ if (!isSqlIdentifier(candidate)) {
5357
+ throw new Error(`Invalid SQL identifier: ${value}`);
5358
+ }
5359
+ return candidate;
5360
+ }
5361
+ function isSupportedAggregateFunction(value) {
5362
+ const fn = String(value ?? "").toLowerCase();
5363
+ return ["count", "sum", "avg", "min", "max"].includes(fn);
5364
+ }
5365
+ function buildAggregateAlias(aggregate, index) {
5366
+ const fn = String(aggregate.fn ?? "").toLowerCase();
5367
+ const hasField = typeof aggregate.field === "string" && aggregate.field.trim().length > 0;
5368
+ return toSafeSqlIdentifier(
5369
+ String(aggregate.as ?? `${fn}_${hasField ? aggregate.field : "all"}_${index}`),
5370
+ `${fn || "aggregate"}_${index}`
5371
+ );
5372
+ }
5373
+ function resolveDataRows(data) {
5374
+ if (!data) {
5375
+ return [];
5376
+ }
5377
+ if (Array.isArray(data)) {
5378
+ return data.map((entry) => ensurePlainObject(entry, "data item"));
5379
+ }
5380
+ return [ensurePlainObject(data, "data")];
5255
5381
  }
5256
5382
  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
5383
  constructor() {
5274
- this.databaseName = "";
5275
- this.dbClient = new import_pg.Pool({
5384
+ this.registrationsByService = /* @__PURE__ */ new Map();
5385
+ this.adminDbClient = new import_pg.Pool({
5276
5386
  connectionString: process.env.DATABASE_ADDRESS ?? "",
5277
5387
  database: "postgres",
5278
5388
  ssl: {
5279
5389
  rejectUnauthorized: false
5280
- // ← This bypasses the chain validation error
5281
5390
  }
5282
5391
  });
5283
- CadenzaService.createMetaRoutine(
5284
- "DatabaseServiceInit",
5285
- [
5286
- // TODO: Database health check
5287
- // TODO: Create database role
5288
- // TODO: Create schema version table
5289
- CadenzaService.createMetaTask(
5290
- "Create database",
5291
- async (ctx) => {
5292
- const { databaseName } = ctx;
5293
- try {
5294
- if (!databaseName.split("").every((c) => /[a-z_]/.test(c))) {
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"
5392
+ CadenzaService.createMetaTask(
5393
+ "Route PostgresActor setup requests",
5394
+ (ctx) => {
5395
+ const serviceName = String(ctx.options?.serviceName ?? ctx.serviceName ?? "");
5396
+ if (!serviceName) {
5397
+ return ctx;
5398
+ }
5399
+ const registration = this.registrationsByService.get(serviceName);
5400
+ if (!registration) {
5401
+ return ctx;
5402
+ }
5403
+ CadenzaService.emit(`meta.postgres_actor.setup_requested.${registration.actorToken}`, 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,704 +5412,1299 @@ var DatabaseController = class _DatabaseController {
5771
5412
  return this._instance;
5772
5413
  }
5773
5414
  reset() {
5774
- this.dbClient.end();
5415
+ for (const registration of this.registrationsByService.values()) {
5416
+ const runtimeState = registration.actor.getRuntimeState(registration.actorKey);
5417
+ if (runtimeState?.pool) {
5418
+ runtimeState.pool.end().catch(() => void 0);
5419
+ }
5420
+ }
5421
+ this.registrationsByService.clear();
5422
+ this.adminDbClient.end().catch(() => void 0);
5775
5423
  }
5776
- /**
5777
- * Asynchronously retrieves a database client from the connection pool with additional logging and timeout capabilities.
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);
5424
+ createPostgresActor(serviceName, schema, options) {
5425
+ const existing = this.registrationsByService.get(serviceName);
5426
+ if (existing) {
5427
+ return existing;
5428
+ }
5429
+ const actorName = `${serviceName}PostgresActor`;
5430
+ const actorToken = normalizeIntentToken(actorName);
5431
+ const actorKey = String(options.databaseName ?? (0, import_lodash_es.snakeCase)(serviceName));
5432
+ const optionTimeout = typeof options.timeoutMs === "number" ? Number(options.timeoutMs) : Number(options.timeout);
5433
+ const safetyPolicy = {
5434
+ statementTimeoutMs: normalizePositiveInteger(
5435
+ optionTimeout,
5436
+ normalizePositiveInteger(Number(process.env.DATABASE_STATEMENT_TIMEOUT_MS ?? 15e3), 15e3)
5437
+ ),
5438
+ retryCount: normalizePositiveInteger(options.retryCount, 3, 0),
5439
+ retryDelayMs: normalizePositiveInteger(Number(process.env.DATABASE_RETRY_DELAY_MS ?? 100), 100),
5440
+ retryDelayMaxMs: normalizePositiveInteger(
5441
+ Number(process.env.DATABASE_RETRY_DELAY_MAX_MS ?? 1e3),
5442
+ 1e3
5443
+ ),
5444
+ retryDelayFactor: Number(process.env.DATABASE_RETRY_DELAY_FACTOR ?? 2)
5801
5445
  };
5802
- client.release = () => {
5803
- clearTimeout(timeout);
5804
- client.query = query;
5805
- client.release = release;
5806
- return release.apply(client);
5446
+ const actor = CadenzaService.createActor(
5447
+ {
5448
+ name: actorName,
5449
+ description: "Specialized PostgresActor owning pool runtime state and schema-driven DB task generation.",
5450
+ defaultKey: actorKey,
5451
+ keyResolver: (input) => typeof input.databaseName === "string" ? input.databaseName : void 0,
5452
+ loadPolicy: "eager",
5453
+ writeContract: "overwrite",
5454
+ initState: {
5455
+ actorName,
5456
+ actorToken,
5457
+ serviceName,
5458
+ databaseName: actorKey,
5459
+ status: "idle",
5460
+ schemaVersion: Number(schema.version ?? 1),
5461
+ setupStartedAt: null,
5462
+ setupCompletedAt: null,
5463
+ lastHealthCheckAt: null,
5464
+ lastError: null,
5465
+ tables: Object.keys(schema.tables ?? {}),
5466
+ safetyPolicy
5467
+ }
5468
+ },
5469
+ { isMeta: Boolean(options.isMeta) }
5470
+ );
5471
+ const registration = {
5472
+ serviceName,
5473
+ databaseName: actorKey,
5474
+ actorName,
5475
+ actorToken,
5476
+ actorKey,
5477
+ actor,
5478
+ schema,
5479
+ options,
5480
+ tasksGenerated: false,
5481
+ intentNames: /* @__PURE__ */ new Set()
5807
5482
  };
5808
- return client;
5483
+ this.registrationsByService.set(serviceName, registration);
5484
+ this.registerSetupTask(registration);
5485
+ return registration;
5809
5486
  }
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"
5487
+ registerSetupTask(registration) {
5488
+ const setupSignal = `meta.postgres_actor.setup_requested.${registration.actorToken}`;
5489
+ CadenzaService.createMetaTask(
5490
+ `Setup ${registration.actorName}`,
5491
+ registration.actor.task(
5492
+ async ({ input, state, runtimeState, setState, setRuntimeState, emit }) => {
5493
+ const requestedDatabaseName = String(
5494
+ input.options?.databaseName ?? input.databaseName ?? registration.databaseName
5823
5495
  );
5824
- return { rows: [] };
5825
- }
5826
- }
5827
- }
5828
- throw new Error(`Timeout waiting for database to be ready`);
5829
- }
5830
- /**
5831
- * Sorts database tables based on their reference dependencies using a topological sort.
5832
- *
5833
- * Tables are reordered such that dependent tables appear later in the list
5834
- * to ensure a dependency hierarchy. If cycles are detected in the dependency graph,
5835
- * they will be noted but the process will not stop. Unreferenced tables are included at the end.
5836
- *
5837
- * @param {Object} ctx - The context object containing the database schema definition and table metadata.
5838
- * ctx.schema {Object} - The schema definition object.
5839
- * ctx.schema.tables {Object} - A mapping of table names to table definitions.
5840
- * Each table definition may contain `fields` (with `references` info)
5841
- * and `foreignKeys` indicating cross-table relationships.
5842
- *
5843
- * @return {Object} - The modified context object with an additional property:
5844
- * sortedTables {string[]} - An array of table names sorted in dependency order.
5845
- * hasCycles {boolean} - Indicates if the dependency graph contains cycles.
5846
- */
5847
- sortTablesByReferences(ctx) {
5848
- const schema = ctx.schema;
5849
- const graph = /* @__PURE__ */ new Map();
5850
- const allTables = Object.keys(schema.tables);
5851
- allTables.forEach((table) => graph.set(table, /* @__PURE__ */ new Set()));
5852
- for (const [tableName, table] of Object.entries(schema.tables)) {
5853
- for (const field of Object.values(table.fields)) {
5854
- if (field.references) {
5855
- const [refTable] = field.references.split("(");
5856
- if (refTable !== tableName && allTables.includes(refTable)) {
5857
- graph.get(refTable)?.add(tableName);
5496
+ if (requestedDatabaseName !== registration.databaseName) {
5497
+ return input;
5858
5498
  }
5859
- }
5860
- }
5861
- if (table.foreignKeys) {
5862
- for (const foreignKey of table.foreignKeys) {
5863
- const refTable = foreignKey.tableName;
5864
- if (refTable !== tableName && allTables.includes(refTable)) {
5865
- graph.get(refTable)?.add(tableName);
5499
+ setState({
5500
+ ...state,
5501
+ status: "initializing",
5502
+ setupStartedAt: (/* @__PURE__ */ new Date()).toISOString(),
5503
+ lastError: null
5504
+ });
5505
+ const priorRuntimePool = runtimeState?.pool ?? null;
5506
+ if (priorRuntimePool) {
5507
+ await priorRuntimePool.end().catch(() => void 0);
5866
5508
  }
5867
- }
5509
+ try {
5510
+ await this.createDatabaseIfMissing(requestedDatabaseName);
5511
+ const pool = this.createTargetPool(
5512
+ requestedDatabaseName,
5513
+ state.safetyPolicy.statementTimeoutMs
5514
+ );
5515
+ await this.checkPoolHealth(pool, state.safetyPolicy);
5516
+ this.validateSchema({
5517
+ schema: registration.schema,
5518
+ options: registration.options
5519
+ });
5520
+ const sortedTables = this.sortTablesByReferences({
5521
+ schema: registration.schema
5522
+ }).sortedTables;
5523
+ const ddlStatements = this.buildSchemaDdlStatements(
5524
+ registration.schema,
5525
+ sortedTables
5526
+ );
5527
+ await this.applyDdlStatements(pool, ddlStatements);
5528
+ if (!registration.tasksGenerated) {
5529
+ this.generateDatabaseTasks(registration);
5530
+ registration.tasksGenerated = true;
5531
+ }
5532
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
5533
+ setRuntimeState({
5534
+ pool,
5535
+ ready: true,
5536
+ pendingQueries: 0,
5537
+ lastHealthCheckAt: Date.now(),
5538
+ lastError: null
5539
+ });
5540
+ setState({
5541
+ ...state,
5542
+ status: "ready",
5543
+ setupCompletedAt: nowIso,
5544
+ lastHealthCheckAt: nowIso,
5545
+ lastError: null,
5546
+ tables: Object.keys(registration.schema.tables ?? {})
5547
+ });
5548
+ emit("meta.database.setup_done", {
5549
+ serviceName: registration.serviceName,
5550
+ databaseName: registration.databaseName,
5551
+ actorName: registration.actorName,
5552
+ __success: true
5553
+ });
5554
+ return {
5555
+ ...input,
5556
+ __success: true,
5557
+ actorName: registration.actorName,
5558
+ databaseName: registration.databaseName
5559
+ };
5560
+ } catch (error) {
5561
+ const message = errorMessage(error);
5562
+ setRuntimeState({
5563
+ pool: null,
5564
+ ready: false,
5565
+ pendingQueries: 0,
5566
+ lastHealthCheckAt: runtimeState?.lastHealthCheckAt ?? null,
5567
+ lastError: message
5568
+ });
5569
+ setState({
5570
+ ...state,
5571
+ status: "error",
5572
+ setupCompletedAt: (/* @__PURE__ */ new Date()).toISOString(),
5573
+ lastError: message
5574
+ });
5575
+ throw error;
5576
+ }
5577
+ },
5578
+ { mode: "write" }
5579
+ ),
5580
+ "Initializes PostgresActor runtime pool, applies schema, and generates CRUD tasks/intents.",
5581
+ { isMeta: true }
5582
+ ).doOn(setupSignal);
5583
+ }
5584
+ createTargetPool(databaseName, statementTimeoutMs) {
5585
+ const connectionString = this.buildDatabaseConnectionString(databaseName);
5586
+ return new import_pg.Pool({
5587
+ connectionString,
5588
+ statement_timeout: statementTimeoutMs,
5589
+ query_timeout: statementTimeoutMs,
5590
+ ssl: {
5591
+ rejectUnauthorized: false
5868
5592
  }
5593
+ });
5594
+ }
5595
+ buildDatabaseConnectionString(databaseName) {
5596
+ const base = process.env.DATABASE_ADDRESS ?? "";
5597
+ if (!base) {
5598
+ throw new Error("DATABASE_ADDRESS environment variable is required");
5869
5599
  }
5870
- const visited = /* @__PURE__ */ new Set();
5871
- const tempMark = /* @__PURE__ */ new Set();
5872
- const sorted = [];
5873
- let hasCycles = false;
5874
- function visit(table) {
5875
- if (tempMark.has(table)) {
5876
- hasCycles = true;
5877
- return;
5600
+ try {
5601
+ const parsed = new URL(base);
5602
+ parsed.pathname = `/${databaseName}`;
5603
+ if (!parsed.searchParams.has("sslmode")) {
5604
+ parsed.searchParams.set("sslmode", "disable");
5878
5605
  }
5879
- if (visited.has(table)) return;
5880
- tempMark.add(table);
5881
- for (const dep of graph.get(table) || []) {
5882
- visit(dep);
5606
+ return parsed.toString();
5607
+ } catch {
5608
+ const lastSlashIndex = base.lastIndexOf("/");
5609
+ if (lastSlashIndex === -1) {
5610
+ throw new Error("DATABASE_ADDRESS must be a valid postgres connection string");
5883
5611
  }
5884
- tempMark.delete(table);
5885
- visited.add(table);
5886
- sorted.push(table);
5612
+ const root = base.slice(0, lastSlashIndex + 1);
5613
+ return `${root}${databaseName}?sslmode=disable`;
5887
5614
  }
5888
- for (const table of allTables) {
5889
- if (!visited.has(table)) {
5890
- visit(table);
5891
- }
5615
+ }
5616
+ async createDatabaseIfMissing(databaseName) {
5617
+ if (!isSqlIdentifier(databaseName)) {
5618
+ throw new Error(
5619
+ `Invalid database name '${databaseName}'. Names must contain only lowercase alphanumerics and underscores and cannot start with a number.`
5620
+ );
5892
5621
  }
5893
- for (const table of allTables) {
5894
- if (!visited.has(table)) {
5895
- sorted.push(table);
5622
+ try {
5623
+ await this.adminDbClient.query(`CREATE DATABASE ${databaseName}`);
5624
+ CadenzaService.log(`Database ${databaseName} created.`);
5625
+ } catch (error) {
5626
+ if (error?.code === "42P04") {
5627
+ CadenzaService.log(`Database ${databaseName} already exists.`);
5628
+ return;
5896
5629
  }
5630
+ throw new Error(`Failed to create database '${databaseName}': ${errorMessage(error)}`);
5897
5631
  }
5898
- sorted.reverse();
5899
- return { ...ctx, sortedTables: sorted, hasCycles };
5900
5632
  }
5901
- /**
5902
- * Asynchronously creates an iterator that splits the provided tables from the schema.
5903
- *
5904
- * @param {Object} ctx - The context object containing the necessary data.
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 };
5633
+ async checkPoolHealth(pool, safetyPolicy) {
5634
+ await this.runWithRetries(
5635
+ async () => {
5636
+ await this.withTimeout(
5637
+ () => pool.query("SELECT 1 as health"),
5638
+ safetyPolicy.statementTimeoutMs,
5639
+ "Database health check timed out"
5640
+ );
5641
+ },
5642
+ safetyPolicy,
5643
+ "Health check"
5644
+ );
5645
+ }
5646
+ getPoolOrThrow(registration) {
5647
+ const runtimeState = registration.actor.getRuntimeState(
5648
+ registration.actorKey
5649
+ );
5650
+ if (!runtimeState || !runtimeState.ready || !runtimeState.pool) {
5651
+ throw new Error(
5652
+ `PostgresActor '${registration.actorName}' is not ready. Ensure setup completed before running DB tasks.`
5653
+ );
5916
5654
  }
5655
+ return runtimeState.pool;
5917
5656
  }
5918
- /**
5919
- * Converts the keys of objects in an array to camelCase format.
5920
- *
5921
- * @param {Array<any>} rows - An array of objects whose keys should be converted to camelCase.
5922
- * @return {Array<any>} A new array of objects with their keys converted to camelCase.
5923
- */
5924
- toCamelCase(rows) {
5925
- return rows.map((row) => {
5926
- const camelCasedRow = {};
5927
- for (const [key, value] of Object.entries(row)) {
5928
- camelCasedRow[(0, import_lodash_es.camelCase)(key)] = value;
5657
+ async withTimeout(work, timeoutMs, timeoutMessage) {
5658
+ let timeoutHandle = null;
5659
+ try {
5660
+ return await Promise.race([
5661
+ work(),
5662
+ new Promise((_, reject) => {
5663
+ timeoutHandle = setTimeout(() => {
5664
+ reject(new Error(timeoutMessage));
5665
+ }, timeoutMs);
5666
+ })
5667
+ ]);
5668
+ } finally {
5669
+ if (timeoutHandle) {
5670
+ clearTimeout(timeoutHandle);
5929
5671
  }
5930
- return camelCasedRow;
5931
- });
5672
+ }
5932
5673
  }
5933
- /**
5934
- * Executes a query against a specified database table with given parameters.
5935
- *
5936
- * @param {string} tableName - The name of the database table to query.
5937
- * @param {DbOperationPayload} context - An object containing query parameters such as filters, fields, joins, sort, limit, and offset.
5938
- * @return {Promise<any>} A promise that resolves with the query result, including rows, row count, and metadata, or an error object if the query fails.
5939
- */
5940
- async queryFunction(tableName, context) {
5674
+ async runWithRetries(work, safetyPolicy, operationLabel) {
5675
+ const attempts = normalizePositiveInteger(safetyPolicy.retryCount, 0, 0) + 1;
5676
+ let delayMs = normalizePositiveInteger(safetyPolicy.retryDelayMs, 100);
5677
+ const maxDelayMs = normalizePositiveInteger(safetyPolicy.retryDelayMaxMs, 1e3);
5678
+ const factor = Number.isFinite(safetyPolicy.retryDelayFactor) ? Math.max(1, safetyPolicy.retryDelayFactor) : 1;
5679
+ let lastError;
5680
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
5681
+ try {
5682
+ return await work();
5683
+ } catch (error) {
5684
+ lastError = error;
5685
+ const transient = isTransientDatabaseError(error);
5686
+ if (!transient || attempt >= attempts) {
5687
+ break;
5688
+ }
5689
+ CadenzaService.log(
5690
+ `${operationLabel} failed with transient error. Retrying...`,
5691
+ {
5692
+ attempt,
5693
+ attempts,
5694
+ delayMs,
5695
+ error: errorMessage(error)
5696
+ },
5697
+ "warning"
5698
+ );
5699
+ await sleep(delayMs);
5700
+ delayMs = Math.min(Math.trunc(delayMs * factor), maxDelayMs);
5701
+ }
5702
+ }
5703
+ throw lastError;
5704
+ }
5705
+ async executeWithTransaction(pool, transaction, callback) {
5706
+ if (!transaction) {
5707
+ return callback(pool);
5708
+ }
5709
+ const client = await pool.connect();
5710
+ try {
5711
+ await client.query("BEGIN");
5712
+ const result = await callback(client);
5713
+ await client.query("COMMIT");
5714
+ return result;
5715
+ } catch (error) {
5716
+ await client.query("ROLLBACK");
5717
+ throw error;
5718
+ } finally {
5719
+ client.release();
5720
+ }
5721
+ }
5722
+ async queryFunction(registration, tableName, context) {
5941
5723
  const {
5942
5724
  filter = {},
5943
5725
  fields = [],
5944
5726
  joins = {},
5945
5727
  sort = {},
5946
5728
  limit,
5947
- offset
5729
+ offset,
5730
+ queryMode = "rows",
5731
+ aggregates = [],
5732
+ groupBy = []
5948
5733
  } = context;
5949
- let sql = `SELECT ${fields.length ? fields.join(", ") : "*"} FROM ${tableName}`;
5734
+ const pool = this.getPoolOrThrow(registration);
5735
+ const statementTimeoutMs = this.resolveSafetyPolicy(registration).statementTimeoutMs;
5736
+ const resolvedMode = queryMode;
5737
+ const aggregateDefinitions = Array.isArray(aggregates) ? aggregates : [];
5738
+ const groupByFields = Array.isArray(groupBy) ? groupBy : [];
5950
5739
  const params = [];
5951
- if (Object.keys(filter).length > 0) {
5952
- sql += " " + this.buildWhereClause(filter, params);
5953
- }
5954
- if (Object.keys(joins).length > 0) {
5955
- sql += " " + this.buildJoinClause(joins);
5740
+ const whereClause = Object.keys(filter).length > 0 ? this.buildWhereClause(filter, params) : "";
5741
+ const joinClause = Object.keys(joins).length > 0 ? this.buildJoinClause(joins) : "";
5742
+ let sql;
5743
+ if (resolvedMode === "count") {
5744
+ sql = `SELECT COUNT(*)::bigint AS count FROM ${tableName} ${joinClause} ${whereClause}`;
5745
+ } else if (resolvedMode === "exists") {
5746
+ sql = `SELECT EXISTS(SELECT 1 FROM ${tableName} ${joinClause} ${whereClause}) AS exists`;
5747
+ } else if (resolvedMode === "aggregate") {
5748
+ if (aggregateDefinitions.length === 0) {
5749
+ throw new Error("Aggregate queries require at least one aggregate definition");
5750
+ }
5751
+ const aggregateExpressions = aggregateDefinitions.map(
5752
+ (aggregate, index) => {
5753
+ const fn = String(aggregate.fn ?? "").toLowerCase();
5754
+ if (!isSupportedAggregateFunction(fn)) {
5755
+ throw new Error(`Unsupported aggregate function '${aggregate.fn}'`);
5756
+ }
5757
+ const hasField = typeof aggregate.field === "string" && aggregate.field.trim().length > 0;
5758
+ if (fn !== "count" && !hasField) {
5759
+ throw new Error(`Aggregate '${fn}' requires a field`);
5760
+ }
5761
+ const fieldExpression = hasField ? (0, import_lodash_es.snakeCase)(String(aggregate.field)) : "*";
5762
+ const distinctPrefix = aggregate.distinct ? "DISTINCT " : "";
5763
+ const expression = fn === "count" && !hasField ? "COUNT(*)" : `${fn.toUpperCase()}(${distinctPrefix}${fieldExpression})`;
5764
+ const alias = buildAggregateAlias(aggregate, index);
5765
+ return `${expression} AS ${alias}`;
5766
+ }
5767
+ );
5768
+ const groupByExpressions = groupByFields.map((field) => (0, import_lodash_es.snakeCase)(field));
5769
+ const selectExpressions = [...groupByExpressions, ...aggregateExpressions];
5770
+ sql = `SELECT ${selectExpressions.join(", ")} FROM ${tableName} ${joinClause} ${whereClause}`;
5771
+ if (groupByExpressions.length > 0) {
5772
+ sql += ` GROUP BY ${groupByExpressions.join(", ")}`;
5773
+ }
5774
+ } else {
5775
+ sql = `SELECT ${fields.length ? fields.map(import_lodash_es.snakeCase).join(", ") : "*"} FROM ${tableName} ${joinClause} ${whereClause}`;
5956
5776
  }
5957
- if (Object.keys(sort).length > 0) {
5958
- sql += " ORDER BY " + Object.entries(sort).map(([field, direction]) => `${field} ${direction}`).join(", ");
5777
+ if (Object.keys(sort).length > 0 && resolvedMode !== "count" && resolvedMode !== "exists") {
5778
+ sql += " ORDER BY " + Object.entries(sort).map(([field, direction]) => `${(0, import_lodash_es.snakeCase)(field)} ${direction}`).join(", ");
5959
5779
  }
5960
- if (limit !== void 0) {
5780
+ if (resolvedMode === "one") {
5781
+ sql += ` LIMIT $${params.length + 1}`;
5782
+ params.push(1);
5783
+ } else if (resolvedMode !== "count" && resolvedMode !== "exists" && limit !== void 0) {
5961
5784
  sql += ` LIMIT $${params.length + 1}`;
5962
5785
  params.push(limit);
5963
5786
  }
5964
- if (offset !== void 0) {
5787
+ if (resolvedMode !== "count" && resolvedMode !== "exists" && offset !== void 0) {
5965
5788
  sql += ` OFFSET $${params.length + 1}`;
5966
5789
  params.push(offset);
5967
5790
  }
5968
5791
  try {
5969
- const result = await this.dbClient.query(sql, params);
5792
+ const result = await this.withTimeout(
5793
+ () => pool.query(sql, params),
5794
+ statementTimeoutMs,
5795
+ `Query timeout on table ${tableName}`
5796
+ );
5970
5797
  const rows = this.toCamelCase(result.rows);
5798
+ const rowCount = Number(result.rowCount ?? 0);
5799
+ if (resolvedMode === "count") {
5800
+ return {
5801
+ count: Number(rows[0]?.count ?? 0),
5802
+ rowCount: Number(rows[0]?.count ?? 0),
5803
+ __success: true
5804
+ };
5805
+ }
5806
+ if (resolvedMode === "exists") {
5807
+ const exists = Boolean(rows[0]?.exists);
5808
+ return {
5809
+ exists,
5810
+ rowCount: exists ? 1 : 0,
5811
+ __success: true
5812
+ };
5813
+ }
5814
+ if (resolvedMode === "one") {
5815
+ return {
5816
+ [`${(0, import_lodash_es.camelCase)(tableName)}`]: rows[0] ?? null,
5817
+ rowCount,
5818
+ __success: true
5819
+ };
5820
+ }
5821
+ if (resolvedMode === "aggregate") {
5822
+ return {
5823
+ aggregates: rows,
5824
+ rowCount,
5825
+ __success: true
5826
+ };
5827
+ }
5971
5828
  return {
5972
5829
  [`${(0, import_lodash_es.camelCase)(tableName)}s`]: rows,
5973
- rowCount: result.rowCount,
5974
- __success: true,
5975
- ...context
5830
+ rowCount,
5831
+ __success: true
5976
5832
  };
5977
5833
  } catch (error) {
5978
5834
  return {
5979
- ...context,
5835
+ rowCount: 0,
5980
5836
  errored: true,
5981
- __error: `Query failed: ${error.message}`,
5837
+ __error: `Query failed: ${errorMessage(error)}`,
5982
5838
  __success: false
5983
5839
  };
5984
5840
  }
5985
5841
  }
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) {
5842
+ async insertFunction(registration, tableName, context) {
6002
5843
  const { data, transaction = true, fields = [], onConflict } = context;
6003
5844
  if (!data || Array.isArray(data) && data.length === 0) {
6004
- return { errored: true, __error: "No data provided for insert" };
5845
+ return {
5846
+ rowCount: 0,
5847
+ errored: true,
5848
+ __error: "No data provided for insert",
5849
+ __success: false
5850
+ };
6005
5851
  }
6006
- let resultContext = {};
6007
- const client = transaction ? await this.getClient() : this.dbClient;
5852
+ const pool = this.getPoolOrThrow(registration);
5853
+ const safetyPolicy = this.resolveSafetyPolicy(registration);
6008
5854
  try {
6009
- if (transaction) await client.query("BEGIN");
6010
- const resolvedData = await this.resolveNestedData(data, tableName);
6011
- const isBatch = Array.isArray(resolvedData);
6012
- const rows = isBatch ? resolvedData : [resolvedData];
6013
- const sql = `INSERT INTO ${tableName} (${Object.keys(rows[0]).map(import_lodash_es.snakeCase).join(", ")}) VALUES `;
6014
- const values = rows.map(
6015
- (row) => `(${Object.values(row).map((value, i) => {
6016
- if (typeof value === "object" && value?.__effect) {
6017
- if (value.__effect === "increment") {
6018
- return `${Object.keys(row)[i]} + 1`;
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
- }
5855
+ const resultContext = await this.runWithRetries(
5856
+ async () => this.executeWithTransaction(pool, Boolean(transaction), async (client) => {
5857
+ const resolvedData = await this.resolveNestedData(
5858
+ registration,
5859
+ data,
5860
+ tableName
5861
+ );
5862
+ const rows = Array.isArray(resolvedData) ? resolvedData : [resolvedData];
5863
+ if (rows.length === 0) {
5864
+ throw new Error("No rows available for insert after resolving data");
6026
5865
  }
6027
- return `$${i + 1}`;
6028
- }).join(", ")})`
6029
- ).join(", ");
6030
- const params = rows.flatMap((row) => Object.values(row));
6031
- let onConflictSql = "";
6032
- if (onConflict) {
6033
- const { target, action } = onConflict;
6034
- onConflictSql += ` ON CONFLICT (${target.join(", ")})`;
6035
- if (action.do === "update") {
6036
- if (!action.set || Object.keys(action.set).length === 0) {
6037
- throw new Error("Update action requires 'set' fields");
5866
+ const keys = Object.keys(rows[0]);
5867
+ const sqlPrefix = `INSERT INTO ${tableName} (${keys.map((key) => (0, import_lodash_es.snakeCase)(key)).join(", ")}) VALUES `;
5868
+ const params = [];
5869
+ const placeholders = rows.map((row) => {
5870
+ const tuple = keys.map((key) => {
5871
+ params.push(row[key]);
5872
+ return `$${params.length}`;
5873
+ }).join(", ");
5874
+ return `(${tuple})`;
5875
+ }).join(", ");
5876
+ let onConflictSql = "";
5877
+ if (onConflict) {
5878
+ onConflictSql = this.buildOnConflictClause(onConflict, params);
6038
5879
  }
6039
- const setClauses = Object.entries(action.set).map(
6040
- ([field, value]) => `${field} = ${value === "excluded" ? "excluded." + field : `$${params.length + 1}`}`
6041
- ).join(", ");
6042
- params.push(
6043
- ...Object.values(action.set).filter(
6044
- (v) => typeof v !== "string" || !v.startsWith("excluded.")
6045
- )
5880
+ const sql = `${sqlPrefix}${placeholders}${onConflictSql} RETURNING ${fields.length ? fields.map(import_lodash_es.snakeCase).join(", ") : "*"}`;
5881
+ const result = await this.withTimeout(
5882
+ () => client.query(sql, params),
5883
+ safetyPolicy.statementTimeoutMs,
5884
+ `Insert timeout on table ${tableName}`
6046
5885
  );
6047
- onConflictSql += ` DO UPDATE SET ${setClauses}`;
6048
- if (action.where) onConflictSql += ` WHERE ${action.where}`;
6049
- } else {
6050
- onConflictSql += ` DO NOTHING`;
6051
- }
6052
- }
6053
- const result = await client.query(
6054
- `${sql} ${values}${onConflictSql} RETURNING ${fields.length ? fields.join(", ") : "*"}`,
6055
- params
5886
+ const resultRows = this.toCamelCase(result.rows);
5887
+ return {
5888
+ [`${(0, import_lodash_es.camelCase)(tableName)}${rows.length > 1 ? "s" : ""}`]: rows.length > 1 ? resultRows : resultRows[0] ?? null,
5889
+ rowCount: result.rowCount,
5890
+ __success: true
5891
+ };
5892
+ }),
5893
+ safetyPolicy,
5894
+ `Insert ${tableName}`
6056
5895
  );
6057
- if (transaction) await client.query("COMMIT");
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
- };
5896
+ return resultContext;
6064
5897
  } catch (error) {
6065
- if (transaction) await client.query("ROLLBACK");
6066
- if (error.message.includes("violates unique constraint")) {
6067
- resultContext = {
6068
- [`${(0, import_lodash_es.camelCase)(tableName)}`]: null,
6069
- __success: false
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
- }
5898
+ return {
5899
+ rowCount: 0,
5900
+ errored: true,
5901
+ __error: `Insert failed: ${errorMessage(error)}`,
5902
+ __success: false
5903
+ };
6083
5904
  }
6084
- return resultContext;
6085
5905
  }
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) {
5906
+ async updateFunction(registration, tableName, context) {
6102
5907
  const { data, filter = {}, transaction = true } = context;
6103
5908
  if (!data || Object.keys(data).length === 0) {
6104
5909
  return {
5910
+ rowCount: 0,
6105
5911
  errored: true,
6106
- __error: `No data provided for update of ${tableName}`
5912
+ __error: `No data provided for update of ${tableName}`,
5913
+ __success: false
6107
5914
  };
6108
5915
  }
6109
- let resultContext = {};
6110
- const client = transaction ? await this.getClient() : this.dbClient;
5916
+ const pool = this.getPoolOrThrow(registration);
5917
+ const safetyPolicy = this.resolveSafetyPolicy(registration);
6111
5918
  try {
6112
- if (transaction) await client.query("BEGIN");
6113
- const resolvedData = await this.resolveNestedData(data, tableName);
6114
- const params = Object.values(resolvedData);
6115
- let offset = 0;
6116
- const setClause = Object.entries(Object.keys(resolvedData)).map(([i, key]) => {
6117
- const value = resolvedData[key];
6118
- const offsetIndex = parseInt(i) - offset;
6119
- if (value.__effect === "increment") {
6120
- params.splice(offsetIndex, 1);
6121
- offset++;
6122
- return `${(0, import_lodash_es.snakeCase)(key)} = ${(0, import_lodash_es.snakeCase)(key)} + 1`;
6123
- }
6124
- if (value.__effect === "decrement") {
6125
- params.splice(offsetIndex, 1);
6126
- offset++;
6127
- return `${(0, import_lodash_es.snakeCase)(key)} = ${(0, import_lodash_es.snakeCase)(key)} - 1`;
6128
- }
6129
- if (value.__effect === "set") {
6130
- params.splice(offsetIndex, 1);
6131
- offset++;
6132
- return `${(0, import_lodash_es.snakeCase)(key)} = ${value.__value}`;
6133
- }
6134
- return `${(0, import_lodash_es.snakeCase)(key)} = $${offsetIndex + 1}`;
6135
- }).join(", ");
6136
- const whereClause = this.buildWhereClause(filter, params);
6137
- const sql = `UPDATE ${tableName} SET ${setClause} ${whereClause} RETURNING *;`;
6138
- const result = await client.query(sql, params);
6139
- if (transaction) await client.query("COMMIT");
6140
- const rows = this.toCamelCase(result.rows);
6141
- if (rows.length === 0) {
6142
- resultContext = {
6143
- sql,
6144
- params,
6145
- __success: false
6146
- };
6147
- } else {
6148
- resultContext = {
6149
- [`${(0, import_lodash_es.camelCase)(tableName)}`]: rows[0],
6150
- __success: true
6151
- };
6152
- }
5919
+ return await this.runWithRetries(
5920
+ async () => this.executeWithTransaction(pool, Boolean(transaction), async (client) => {
5921
+ const resolvedData = await this.resolveNestedData(
5922
+ registration,
5923
+ data,
5924
+ tableName
5925
+ );
5926
+ const params = Object.values(resolvedData);
5927
+ let offset = 0;
5928
+ const setClause = Object.keys(resolvedData).map((key, index) => {
5929
+ const value = resolvedData[key];
5930
+ const offsetIndex = index - offset;
5931
+ if (value?.__effect === "increment") {
5932
+ params.splice(offsetIndex, 1);
5933
+ offset += 1;
5934
+ return `${(0, import_lodash_es.snakeCase)(key)} = ${(0, import_lodash_es.snakeCase)(key)} + 1`;
5935
+ }
5936
+ if (value?.__effect === "decrement") {
5937
+ params.splice(offsetIndex, 1);
5938
+ offset += 1;
5939
+ return `${(0, import_lodash_es.snakeCase)(key)} = ${(0, import_lodash_es.snakeCase)(key)} - 1`;
5940
+ }
5941
+ if (value?.__effect === "set") {
5942
+ params.splice(offsetIndex, 1);
5943
+ offset += 1;
5944
+ return `${(0, import_lodash_es.snakeCase)(key)} = ${value.__value}`;
5945
+ }
5946
+ return `${(0, import_lodash_es.snakeCase)(key)} = $${offsetIndex + 1}`;
5947
+ }).join(", ");
5948
+ const whereClause = this.buildWhereClause(filter, params);
5949
+ const sql = `UPDATE ${tableName} SET ${setClause} ${whereClause} RETURNING *`;
5950
+ const result = await this.withTimeout(
5951
+ () => client.query(sql, params),
5952
+ safetyPolicy.statementTimeoutMs,
5953
+ `Update timeout on table ${tableName}`
5954
+ );
5955
+ const rows = this.toCamelCase(result.rows);
5956
+ const rowCount = Number(result.rowCount ?? 0);
5957
+ return {
5958
+ [`${(0, import_lodash_es.camelCase)(tableName)}`]: rows[0] ?? null,
5959
+ rowCount,
5960
+ __success: rowCount > 0
5961
+ };
5962
+ }),
5963
+ safetyPolicy,
5964
+ `Update ${tableName}`
5965
+ );
6153
5966
  } catch (error) {
6154
- if (transaction) await client.query("ROLLBACK");
6155
- resultContext = {
6156
- ...context,
5967
+ return {
5968
+ rowCount: 0,
6157
5969
  errored: true,
6158
- __error: `Update failed: ${error.message}`,
5970
+ __error: `Update failed: ${errorMessage(error)}`,
6159
5971
  __success: false
6160
5972
  };
6161
- } finally {
6162
- if (transaction && client) {
6163
- client.release();
6164
- }
6165
5973
  }
6166
- return resultContext;
6167
5974
  }
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) {
5975
+ async deleteFunction(registration, tableName, context) {
6179
5976
  const { filter = {}, transaction = true } = context;
6180
5977
  if (Object.keys(filter).length === 0) {
6181
- return { errored: true, __error: "No filter provided for delete" };
5978
+ return {
5979
+ rowCount: 0,
5980
+ errored: true,
5981
+ __error: "No filter provided for delete",
5982
+ __success: false
5983
+ };
6182
5984
  }
6183
- let resultContext = {};
6184
- const client = transaction ? await this.getClient() : this.dbClient;
5985
+ const pool = this.getPoolOrThrow(registration);
5986
+ const safetyPolicy = this.resolveSafetyPolicy(registration);
6185
5987
  try {
6186
- if (transaction) await client.query("BEGIN");
6187
- const params = [];
6188
- const whereClause = this.buildWhereClause(filter, params);
6189
- const sql = `DELETE FROM ${tableName} ${whereClause} RETURNING *`;
6190
- const result = await client.query(sql, params);
6191
- if (transaction) await client.query("COMMIT");
6192
- const rows = this.toCamelCase(result.rows);
6193
- resultContext = {
6194
- [`${(0, import_lodash_es.camelCase)(tableName)}`]: rows[0],
6195
- __success: true
6196
- };
5988
+ return await this.runWithRetries(
5989
+ async () => this.executeWithTransaction(pool, Boolean(transaction), async (client) => {
5990
+ const params = [];
5991
+ const whereClause = this.buildWhereClause(filter, params);
5992
+ const sql = `DELETE FROM ${tableName} ${whereClause} RETURNING *`;
5993
+ const result = await this.withTimeout(
5994
+ () => client.query(sql, params),
5995
+ safetyPolicy.statementTimeoutMs,
5996
+ `Delete timeout on table ${tableName}`
5997
+ );
5998
+ const rows = this.toCamelCase(result.rows);
5999
+ return {
6000
+ [`${(0, import_lodash_es.camelCase)(tableName)}`]: rows[0] ?? null,
6001
+ rowCount: result.rowCount,
6002
+ __success: true
6003
+ };
6004
+ }),
6005
+ safetyPolicy,
6006
+ `Delete ${tableName}`
6007
+ );
6197
6008
  } catch (error) {
6198
- if (transaction) await client.query("ROLLBACK");
6199
- resultContext = {
6009
+ return {
6010
+ rowCount: 0,
6200
6011
  errored: true,
6201
- __error: `Delete failed: ${error.message}`,
6202
- __errors: { delete: error.message },
6012
+ __error: `Delete failed: ${errorMessage(error)}`,
6203
6013
  __success: false
6204
6014
  };
6205
- } finally {
6206
- if (transaction && client) {
6207
- client.release();
6015
+ }
6016
+ }
6017
+ resolveSafetyPolicy(registration) {
6018
+ const durableState = registration.actor.getState(
6019
+ registration.actorKey
6020
+ );
6021
+ return {
6022
+ statementTimeoutMs: normalizePositiveInteger(
6023
+ durableState.safetyPolicy?.statementTimeoutMs,
6024
+ 15e3
6025
+ ),
6026
+ retryCount: normalizePositiveInteger(durableState.safetyPolicy?.retryCount, 3, 0),
6027
+ retryDelayMs: normalizePositiveInteger(durableState.safetyPolicy?.retryDelayMs, 100),
6028
+ retryDelayMaxMs: normalizePositiveInteger(
6029
+ durableState.safetyPolicy?.retryDelayMaxMs,
6030
+ 1e3
6031
+ ),
6032
+ retryDelayFactor: Number.isFinite(durableState.safetyPolicy?.retryDelayFactor) ? Math.max(1, Number(durableState.safetyPolicy?.retryDelayFactor)) : 1
6033
+ };
6034
+ }
6035
+ buildOnConflictClause(onConflict, params) {
6036
+ const { target, action } = onConflict;
6037
+ let sql = ` ON CONFLICT (${target.map(import_lodash_es.snakeCase).join(", ")})`;
6038
+ if (action.do === "update") {
6039
+ if (!action.set || Object.keys(action.set).length === 0) {
6040
+ throw new Error("Update action requires 'set' fields");
6041
+ }
6042
+ const assignments = Object.entries(action.set).map(([field, value]) => {
6043
+ if (typeof value === "string" && value === "excluded") {
6044
+ return `${(0, import_lodash_es.snakeCase)(field)} = excluded.${(0, import_lodash_es.snakeCase)(field)}`;
6045
+ }
6046
+ params.push(value);
6047
+ return `${(0, import_lodash_es.snakeCase)(field)} = $${params.length}`;
6048
+ });
6049
+ sql += ` DO UPDATE SET ${assignments.join(", ")}`;
6050
+ if (action.where) {
6051
+ sql += ` WHERE ${action.where}`;
6208
6052
  }
6053
+ return sql;
6209
6054
  }
6210
- return resultContext;
6055
+ sql += " DO NOTHING";
6056
+ return sql;
6211
6057
  }
6212
6058
  /**
6213
- * Constructs a SQL WHERE clause based on the provided filter object.
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.
6059
+ * Validates database schema structure and content.
6224
6060
  */
6061
+ validateSchema(ctx) {
6062
+ const schema = ctx.schema;
6063
+ if (!schema?.tables || typeof schema.tables !== "object") {
6064
+ throw new Error("Invalid schema: missing or invalid tables");
6065
+ }
6066
+ for (const [tableName, table] of Object.entries(schema.tables)) {
6067
+ if (!isSqlIdentifier(tableName)) {
6068
+ throw new Error(
6069
+ `Invalid table name ${tableName}. Table names must use lowercase snake_case identifiers.`
6070
+ );
6071
+ }
6072
+ if (!table.fields || typeof table.fields !== "object") {
6073
+ throw new Error(`Invalid table ${tableName}: missing fields`);
6074
+ }
6075
+ for (const [fieldName, field] of Object.entries(table.fields)) {
6076
+ if (!isSqlIdentifier(fieldName)) {
6077
+ throw new Error(
6078
+ `Invalid field name ${fieldName} for ${tableName}. Field names must use lowercase snake_case identifiers.`
6079
+ );
6080
+ }
6081
+ if (!SCHEMA_TYPES.includes(field.type)) {
6082
+ throw new Error(`Invalid type ${field.type} for ${tableName}.${fieldName}`);
6083
+ }
6084
+ if (field.references && !field.references.match(/^[\w]+\([\w]+\)$/)) {
6085
+ throw new Error(
6086
+ `Invalid reference ${field.references} for ${tableName}.${fieldName}`
6087
+ );
6088
+ }
6089
+ }
6090
+ for (const operation of ["query", "insert", "update", "delete"]) {
6091
+ const customIntents = table.customIntents?.[operation] ?? [];
6092
+ if (!Array.isArray(customIntents)) {
6093
+ throw new Error(
6094
+ `Invalid customIntents.${operation} for table ${tableName}: expected array`
6095
+ );
6096
+ }
6097
+ for (const customIntent of customIntents) {
6098
+ const parsed = readCustomIntentConfig(
6099
+ customIntent
6100
+ );
6101
+ validateIntentName(String(parsed.intent ?? ""));
6102
+ }
6103
+ }
6104
+ }
6105
+ return true;
6106
+ }
6107
+ sortTablesByReferences(ctx) {
6108
+ const schema = ctx.schema;
6109
+ const graph = /* @__PURE__ */ new Map();
6110
+ const allTables = Object.keys(schema.tables);
6111
+ allTables.forEach((table) => graph.set(table, /* @__PURE__ */ new Set()));
6112
+ for (const [tableName, table] of Object.entries(schema.tables)) {
6113
+ for (const field of Object.values(table.fields)) {
6114
+ if (field.references) {
6115
+ const [refTable] = field.references.split("(");
6116
+ if (refTable !== tableName && allTables.includes(refTable)) {
6117
+ graph.get(refTable)?.add(tableName);
6118
+ }
6119
+ }
6120
+ }
6121
+ if (table.foreignKeys) {
6122
+ for (const foreignKey of table.foreignKeys) {
6123
+ const refTable = foreignKey.tableName;
6124
+ if (refTable !== tableName && allTables.includes(refTable)) {
6125
+ graph.get(refTable)?.add(tableName);
6126
+ }
6127
+ }
6128
+ }
6129
+ }
6130
+ const visited = /* @__PURE__ */ new Set();
6131
+ const tempMark = /* @__PURE__ */ new Set();
6132
+ const sorted = [];
6133
+ let hasCycles = false;
6134
+ const visit = (table) => {
6135
+ if (tempMark.has(table)) {
6136
+ hasCycles = true;
6137
+ return;
6138
+ }
6139
+ if (visited.has(table)) return;
6140
+ tempMark.add(table);
6141
+ for (const dependent of graph.get(table) || []) {
6142
+ visit(dependent);
6143
+ }
6144
+ tempMark.delete(table);
6145
+ visited.add(table);
6146
+ sorted.push(table);
6147
+ };
6148
+ for (const table of allTables) {
6149
+ if (!visited.has(table)) {
6150
+ visit(table);
6151
+ }
6152
+ }
6153
+ for (const table of allTables) {
6154
+ if (!visited.has(table)) {
6155
+ sorted.push(table);
6156
+ }
6157
+ }
6158
+ sorted.reverse();
6159
+ return { ...ctx, sortedTables: sorted, hasCycles };
6160
+ }
6161
+ buildSchemaDdlStatements(schema, sortedTables) {
6162
+ const ddl = [];
6163
+ for (const tableName of sortedTables) {
6164
+ const table = schema.tables[tableName];
6165
+ const fieldDefs = Object.entries(table.fields).map(([fieldName, field]) => this.fieldDefinitionToSql(fieldName, field)).join(", ");
6166
+ ddl.push(`CREATE TABLE IF NOT EXISTS ${tableName} (${fieldDefs});`);
6167
+ for (const indexFields of table.indexes ?? []) {
6168
+ ddl.push(
6169
+ `CREATE INDEX IF NOT EXISTS idx_${tableName}_${indexFields.join("_")} ON ${tableName} (${indexFields.map(import_lodash_es.snakeCase).join(", ")});`
6170
+ );
6171
+ }
6172
+ if (table.primaryKey) {
6173
+ ddl.push(
6174
+ `ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS pk_${tableName}_${table.primaryKey.join("_")};`,
6175
+ `ALTER TABLE ${tableName} ADD CONSTRAINT pk_${tableName}_${table.primaryKey.join("_")} PRIMARY KEY (${table.primaryKey.map(import_lodash_es.snakeCase).join(", ")});`
6176
+ );
6177
+ }
6178
+ for (const uniqueFields of table.uniqueConstraints ?? []) {
6179
+ ddl.push(
6180
+ `ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS uq_${tableName}_${uniqueFields.join("_")};`,
6181
+ `ALTER TABLE ${tableName} ADD CONSTRAINT uq_${tableName}_${uniqueFields.join("_")} UNIQUE (${uniqueFields.map(import_lodash_es.snakeCase).join(", ")});`
6182
+ );
6183
+ }
6184
+ for (const foreignKey of table.foreignKeys ?? []) {
6185
+ const fkName = `fk_${tableName}_${foreignKey.fields.join("_")}`;
6186
+ ddl.push(
6187
+ `ALTER TABLE ${tableName} DROP CONSTRAINT IF EXISTS ${fkName};`,
6188
+ `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(", ")});`
6189
+ );
6190
+ }
6191
+ for (const [triggerName, trigger] of Object.entries(table.triggers ?? {})) {
6192
+ ddl.push(
6193
+ `CREATE OR REPLACE TRIGGER ${triggerName} ${trigger.when} ${trigger.event} ON ${tableName} FOR EACH STATEMENT EXECUTE FUNCTION ${trigger.function};`
6194
+ );
6195
+ }
6196
+ if (table.initialData) {
6197
+ ddl.push(
6198
+ `INSERT INTO ${tableName} (${table.initialData.fields.map(import_lodash_es.snakeCase).join(", ")}) VALUES ${table.initialData.data.map(
6199
+ (row) => `(${row.map((value) => {
6200
+ if (value === void 0) return "NULL";
6201
+ if (value === null) return "NULL";
6202
+ if (typeof value === "number") return String(value);
6203
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
6204
+ const stringValue = String(value);
6205
+ return `'${stringValue.replace(/'/g, "''")}'`;
6206
+ }).join(", ")})`
6207
+ ).join(", ")} ON CONFLICT DO NOTHING;`
6208
+ );
6209
+ }
6210
+ }
6211
+ return ddl;
6212
+ }
6213
+ fieldDefinitionToSql(fieldName, field) {
6214
+ let definition = `${(0, import_lodash_es.snakeCase)(fieldName)} ${field.type.toUpperCase()}`;
6215
+ if (field.type === "varchar") {
6216
+ definition += `(${field.constraints?.maxLength ?? 255})`;
6217
+ }
6218
+ if (field.type === "decimal") {
6219
+ definition += `(${field.constraints?.precision ?? 10},${field.constraints?.scale ?? 2})`;
6220
+ }
6221
+ if (field.primary) definition += " PRIMARY KEY";
6222
+ if (field.unique) definition += " UNIQUE";
6223
+ if (field.default !== void 0) {
6224
+ definition += ` DEFAULT ${field.default === "" ? "''" : String(field.default)}`;
6225
+ }
6226
+ if (field.required && !field.nullable) definition += " NOT NULL";
6227
+ if (field.nullable) definition += " NULL";
6228
+ if (field.generated) {
6229
+ definition += ` GENERATED ALWAYS AS ${field.generated.toUpperCase()} STORED`;
6230
+ }
6231
+ if (field.references) {
6232
+ definition += ` REFERENCES ${field.references} ON DELETE ${field.onDelete || "CASCADE"}`;
6233
+ }
6234
+ if (field.constraints?.check) {
6235
+ definition += ` CHECK (${field.constraints.check})`;
6236
+ }
6237
+ return definition;
6238
+ }
6239
+ async applyDdlStatements(pool, statements) {
6240
+ for (const sql of statements) {
6241
+ try {
6242
+ await pool.query(sql);
6243
+ } catch (error) {
6244
+ CadenzaService.log(
6245
+ "Error applying DDL statement",
6246
+ {
6247
+ sql,
6248
+ error: errorMessage(error)
6249
+ },
6250
+ "error"
6251
+ );
6252
+ throw error;
6253
+ }
6254
+ }
6255
+ }
6256
+ generateDatabaseTasks(registration) {
6257
+ for (const [tableName, table] of Object.entries(registration.schema.tables)) {
6258
+ this.createDatabaseTask(registration, "query", tableName, table);
6259
+ this.createDatabaseTask(registration, "insert", tableName, table);
6260
+ this.createDatabaseTask(registration, "update", tableName, table);
6261
+ this.createDatabaseTask(registration, "delete", tableName, table);
6262
+ this.createDatabaseMacroTasks(registration, tableName, table);
6263
+ }
6264
+ }
6265
+ createDatabaseMacroTasks(registration, tableName, table) {
6266
+ const querySchema = this.getInputSchema("query", tableName, table);
6267
+ const insertSchema = this.getInputSchema("insert", tableName, table);
6268
+ const queryMacroOperations = [
6269
+ "count",
6270
+ "exists",
6271
+ "one",
6272
+ "aggregate"
6273
+ ];
6274
+ for (const macroOperation of queryMacroOperations) {
6275
+ const intentName = `${macroOperation}-pg-${registration.actorToken}-${tableName}`;
6276
+ if (registration.intentNames.has(intentName)) {
6277
+ throw new Error(
6278
+ `Duplicate macro intent '${intentName}' detected for table '${tableName}' in actor '${registration.actorName}'`
6279
+ );
6280
+ }
6281
+ registration.intentNames.add(intentName);
6282
+ CadenzaService.defineIntent({
6283
+ name: intentName,
6284
+ description: `Macro ${macroOperation} operation for table ${tableName}`,
6285
+ input: querySchema
6286
+ });
6287
+ CadenzaService.createThrottledTask(
6288
+ `${macroOperation.toUpperCase()} ${tableName}`,
6289
+ registration.actor.task(
6290
+ async ({ input }) => {
6291
+ const payload = typeof input.queryData === "object" && input.queryData ? input.queryData : input;
6292
+ const result = await this.queryFunction(registration, tableName, {
6293
+ ...payload,
6294
+ queryMode: macroOperation
6295
+ });
6296
+ return {
6297
+ ...input,
6298
+ ...result
6299
+ };
6300
+ },
6301
+ { mode: "read" }
6302
+ ),
6303
+ (context) => context?.__metadata?.__executionTraceId ?? context?.__executionTraceId ?? "default",
6304
+ `Macro ${macroOperation} task for ${tableName}`,
6305
+ {
6306
+ isMeta: registration.options.isMeta,
6307
+ isSubMeta: registration.options.isMeta,
6308
+ validateInputContext: registration.options.securityProfile !== "low",
6309
+ inputSchema: querySchema
6310
+ }
6311
+ ).respondsTo(intentName);
6312
+ }
6313
+ const upsertIntentName = `upsert-pg-${registration.actorToken}-${tableName}`;
6314
+ if (registration.intentNames.has(upsertIntentName)) {
6315
+ throw new Error(
6316
+ `Duplicate macro intent '${upsertIntentName}' detected for table '${tableName}' in actor '${registration.actorName}'`
6317
+ );
6318
+ }
6319
+ registration.intentNames.add(upsertIntentName);
6320
+ CadenzaService.defineIntent({
6321
+ name: upsertIntentName,
6322
+ description: `Macro upsert operation for table ${tableName}`,
6323
+ input: insertSchema
6324
+ });
6325
+ CadenzaService.createThrottledTask(
6326
+ `UPSERT ${tableName}`,
6327
+ registration.actor.task(
6328
+ async ({ input }) => {
6329
+ const payload = typeof input.queryData === "object" && input.queryData ? input.queryData : input;
6330
+ if (!payload.onConflict) {
6331
+ return {
6332
+ ...input,
6333
+ errored: true,
6334
+ __success: false,
6335
+ __error: `Macro upsert requires 'onConflict' payload for table '${tableName}'`
6336
+ };
6337
+ }
6338
+ const result = await this.insertFunction(registration, tableName, payload);
6339
+ return {
6340
+ ...input,
6341
+ ...result
6342
+ };
6343
+ },
6344
+ { mode: "write" }
6345
+ ),
6346
+ (context) => context?.__metadata?.__executionTraceId ?? context?.__executionTraceId ?? "default",
6347
+ `Macro upsert task for ${tableName}`,
6348
+ {
6349
+ isMeta: registration.options.isMeta,
6350
+ isSubMeta: registration.options.isMeta,
6351
+ validateInputContext: registration.options.securityProfile !== "low",
6352
+ inputSchema: insertSchema
6353
+ }
6354
+ ).respondsTo(upsertIntentName);
6355
+ }
6356
+ createDatabaseTask(registration, op, tableName, table) {
6357
+ const opAction = op === "query" ? "queried" : op === "insert" ? "inserted" : op === "update" ? "updated" : "deleted";
6358
+ const defaultSignal = `global.${registration.options.isMeta ? "meta." : ""}${tableName}.${opAction}`;
6359
+ const taskName = `${op.charAt(0).toUpperCase() + op.slice(1)} ${tableName}`;
6360
+ const schema = this.getInputSchema(op, tableName, table);
6361
+ const databaseTaskFunction = registration.actor.task(
6362
+ async ({ input, emit }) => {
6363
+ let context = { ...input };
6364
+ let payloadModifiedByTriggers = false;
6365
+ for (const action of Object.keys(table.customSignals?.triggers ?? {})) {
6366
+ const triggerDefinitions = table.customSignals?.triggers?.[action];
6367
+ for (const trigger of triggerDefinitions ?? []) {
6368
+ if (typeof trigger === "string") {
6369
+ continue;
6370
+ }
6371
+ if (trigger.condition && !trigger.condition(context)) {
6372
+ return {
6373
+ failed: true,
6374
+ __success: false,
6375
+ __error: `Condition for signal trigger failed: ${trigger.signal}`
6376
+ };
6377
+ }
6378
+ if (trigger.queryData) {
6379
+ context.queryData = {
6380
+ ...context.queryData ?? {},
6381
+ ...trigger.queryData
6382
+ };
6383
+ payloadModifiedByTriggers = true;
6384
+ }
6385
+ }
6386
+ }
6387
+ const operationPayload = typeof context.queryData === "object" && context.queryData ? context.queryData : context;
6388
+ this.validateOperationPayload(
6389
+ registration,
6390
+ op,
6391
+ tableName,
6392
+ table,
6393
+ operationPayload,
6394
+ {
6395
+ enforceFieldAllowlist: registration.options.securityProfile === "low" || payloadModifiedByTriggers
6396
+ }
6397
+ );
6398
+ let result;
6399
+ if (op === "query") {
6400
+ result = await this.queryFunction(registration, tableName, operationPayload);
6401
+ } else if (op === "insert") {
6402
+ result = await this.insertFunction(registration, tableName, operationPayload);
6403
+ } else if (op === "update") {
6404
+ result = await this.updateFunction(registration, tableName, operationPayload);
6405
+ } else {
6406
+ result = await this.deleteFunction(registration, tableName, operationPayload);
6407
+ }
6408
+ context = {
6409
+ ...context,
6410
+ ...result
6411
+ };
6412
+ if (!context.errored) {
6413
+ for (const signal of table.customSignals?.emissions?.[op] ?? []) {
6414
+ if (typeof signal === "string") {
6415
+ emit(signal, context);
6416
+ continue;
6417
+ }
6418
+ if (signal.condition && !signal.condition(context)) {
6419
+ continue;
6420
+ }
6421
+ emit(signal.signal, context);
6422
+ }
6423
+ }
6424
+ if (tableName !== "system_log" && context.errored) {
6425
+ CadenzaService.log(
6426
+ `ERROR in ${taskName}`,
6427
+ JSON.stringify({
6428
+ data: context.data,
6429
+ queryData: context.queryData,
6430
+ filter: context.filter,
6431
+ fields: context.fields,
6432
+ joins: context.joins,
6433
+ sort: context.sort,
6434
+ limit: context.limit,
6435
+ offset: context.offset,
6436
+ error: context.__error
6437
+ }),
6438
+ "error"
6439
+ );
6440
+ }
6441
+ delete context.queryData;
6442
+ delete context.data;
6443
+ delete context.filter;
6444
+ delete context.fields;
6445
+ delete context.joins;
6446
+ delete context.sort;
6447
+ delete context.limit;
6448
+ delete context.offset;
6449
+ return context;
6450
+ },
6451
+ { mode: op === "query" ? "read" : "write" }
6452
+ );
6453
+ const task = CadenzaService.createThrottledTask(
6454
+ taskName,
6455
+ databaseTaskFunction,
6456
+ (context) => context?.__metadata?.__executionTraceId ?? context?.__executionTraceId ?? "default",
6457
+ `Auto-generated ${op} task for ${tableName} (PostgresActor)`,
6458
+ {
6459
+ isMeta: registration.options.isMeta,
6460
+ isSubMeta: registration.options.isMeta,
6461
+ validateInputContext: registration.options.securityProfile !== "low",
6462
+ inputSchema: schema
6463
+ }
6464
+ ).doOn(
6465
+ ...table.customSignals?.triggers?.[op]?.map(
6466
+ (signal) => typeof signal === "string" ? signal : signal.signal
6467
+ ) ?? []
6468
+ ).emits(defaultSignal).attachSignal(
6469
+ ...table.customSignals?.emissions?.[op]?.map(
6470
+ (signal) => typeof signal === "string" ? signal : signal.signal
6471
+ ) ?? []
6472
+ );
6473
+ const { intents } = resolveTableOperationIntents(
6474
+ registration.actorName,
6475
+ tableName,
6476
+ table,
6477
+ op,
6478
+ schema
6479
+ );
6480
+ for (const intent of intents) {
6481
+ if (registration.intentNames.has(intent.name)) {
6482
+ throw new Error(
6483
+ `Duplicate auto/custom intent '${intent.name}' detected while generating ${op} task for table '${tableName}' in actor '${registration.actorName}'`
6484
+ );
6485
+ }
6486
+ registration.intentNames.add(intent.name);
6487
+ CadenzaService.defineIntent({
6488
+ name: intent.name,
6489
+ description: intent.description,
6490
+ input: intent.input
6491
+ });
6492
+ }
6493
+ task.respondsTo(...intents.map((intent) => intent.name));
6494
+ }
6495
+ validateOperationPayload(registration, operation, tableName, table, payload, options) {
6496
+ const allowedFields = new Set(Object.keys(table.fields));
6497
+ const resolvedMode = payload.queryMode ?? "rows";
6498
+ if (!["rows", "count", "exists", "one", "aggregate"].includes(resolvedMode)) {
6499
+ throw new Error(`Unsupported queryMode '${String(payload.queryMode)}'`);
6500
+ }
6501
+ const assertAllowedField = (fieldName, label) => {
6502
+ if (!allowedFields.has(fieldName)) {
6503
+ throw new Error(
6504
+ `Invalid field '${fieldName}' in ${label} for ${operation} on ${tableName}`
6505
+ );
6506
+ }
6507
+ };
6508
+ const aggregateDefinitions = Array.isArray(payload.aggregates) ? payload.aggregates : [];
6509
+ const aggregateSortAllowlist = /* @__PURE__ */ new Set();
6510
+ if (resolvedMode === "aggregate") {
6511
+ if (aggregateDefinitions.length === 0) {
6512
+ throw new Error(
6513
+ `Aggregate queryMode requires at least one aggregate on table '${tableName}'`
6514
+ );
6515
+ }
6516
+ for (const groupField of payload.groupBy ?? []) {
6517
+ assertAllowedField(groupField, "groupBy");
6518
+ aggregateSortAllowlist.add(groupField);
6519
+ }
6520
+ for (const [index, aggregate] of aggregateDefinitions.entries()) {
6521
+ if (!isSupportedAggregateFunction(aggregate.fn)) {
6522
+ throw new Error(
6523
+ `Unsupported aggregate function '${String(aggregate.fn)}' on table '${tableName}'`
6524
+ );
6525
+ }
6526
+ if (aggregate.fn !== "count" && !aggregate.field) {
6527
+ throw new Error(
6528
+ `Aggregate '${aggregate.fn}' requires field on table '${tableName}'`
6529
+ );
6530
+ }
6531
+ if (aggregate.field) {
6532
+ assertAllowedField(aggregate.field, "aggregates.field");
6533
+ }
6534
+ aggregateSortAllowlist.add(buildAggregateAlias(aggregate, index));
6535
+ }
6536
+ } else if (aggregateDefinitions.length > 0 || (payload.groupBy ?? []).length > 0) {
6537
+ throw new Error(
6538
+ `aggregates/groupBy payload requires queryMode='aggregate' on table '${tableName}'`
6539
+ );
6540
+ }
6541
+ if (options.enforceFieldAllowlist) {
6542
+ if (payload.fields) {
6543
+ for (const field of payload.fields) {
6544
+ assertAllowedField(field, "fields");
6545
+ }
6546
+ }
6547
+ if (payload.filter) {
6548
+ for (const field of Object.keys(payload.filter)) {
6549
+ assertAllowedField(field, "filter");
6550
+ }
6551
+ }
6552
+ if (payload.data) {
6553
+ const rows = resolveDataRows(payload.data);
6554
+ for (const row of rows) {
6555
+ for (const field of Object.keys(row)) {
6556
+ assertAllowedField(field, "data");
6557
+ }
6558
+ }
6559
+ }
6560
+ }
6561
+ if (payload.sort) {
6562
+ for (const field of Object.keys(payload.sort)) {
6563
+ if (resolvedMode === "aggregate" && aggregateSortAllowlist.has(field)) {
6564
+ continue;
6565
+ }
6566
+ assertAllowedField(field, "sort");
6567
+ }
6568
+ }
6569
+ if (payload.onConflict) {
6570
+ for (const conflictField of payload.onConflict.target ?? []) {
6571
+ assertAllowedField(conflictField, "onConflict.target");
6572
+ }
6573
+ for (const setField of Object.keys(payload.onConflict.action?.set ?? {})) {
6574
+ assertAllowedField(setField, "onConflict.action.set");
6575
+ }
6576
+ }
6577
+ if (payload.joins) {
6578
+ this.validateJoinPayload(registration.schema, payload.joins);
6579
+ }
6580
+ }
6581
+ validateJoinPayload(schema, joins) {
6582
+ for (const [joinTableName, joinDefinition] of Object.entries(joins)) {
6583
+ if (!schema.tables[joinTableName]) {
6584
+ throw new Error(`Invalid join table '${joinTableName}'. Table does not exist in schema.`);
6585
+ }
6586
+ const joinTable = schema.tables[joinTableName];
6587
+ for (const field of joinDefinition.fields ?? []) {
6588
+ if (!joinTable.fields[field]) {
6589
+ throw new Error(
6590
+ `Invalid join field '${field}' on joined table '${joinTableName}'`
6591
+ );
6592
+ }
6593
+ }
6594
+ if (joinDefinition.filter) {
6595
+ for (const filterField of Object.keys(joinDefinition.filter)) {
6596
+ if (!joinTable.fields[filterField]) {
6597
+ throw new Error(
6598
+ `Invalid join filter field '${filterField}' on joined table '${joinTableName}'`
6599
+ );
6600
+ }
6601
+ }
6602
+ }
6603
+ if (joinDefinition.joins) {
6604
+ this.validateJoinPayload(schema, joinDefinition.joins);
6605
+ }
6606
+ }
6607
+ }
6608
+ toCamelCase(rows) {
6609
+ return rows.map((row) => {
6610
+ const camelCasedRow = {};
6611
+ for (const [key, value] of Object.entries(row)) {
6612
+ camelCasedRow[(0, import_lodash_es.camelCase)(key)] = value;
6613
+ }
6614
+ return camelCasedRow;
6615
+ });
6616
+ }
6225
6617
  buildWhereClause(filter, params) {
6226
6618
  const conditions = [];
6227
6619
  for (const [key, value] of Object.entries(filter)) {
6228
6620
  if (value !== void 0) {
6229
6621
  if (Array.isArray(value)) {
6230
- conditions.push(
6231
- `${(0, import_lodash_es.snakeCase)(key)} IN (${value.map((v) => {
6232
- const val = `$${params.length + 1}`;
6233
- params.push(v);
6234
- return val;
6235
- }).join(", ")})`
6236
- );
6622
+ const placeholders = value.map((entry) => {
6623
+ params.push(entry);
6624
+ return `$${params.length}`;
6625
+ }).join(", ");
6626
+ conditions.push(`${(0, import_lodash_es.snakeCase)(key)} IN (${placeholders})`);
6237
6627
  } else {
6238
- conditions.push(`${(0, import_lodash_es.snakeCase)(key)} = $${params.length + 1}`);
6239
6628
  params.push(value);
6629
+ conditions.push(`${(0, import_lodash_es.snakeCase)(key)} = $${params.length}`);
6240
6630
  }
6241
6631
  }
6242
6632
  }
6243
6633
  return conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
6244
6634
  }
6245
- /**
6246
- * Constructs a SQL join clause from a given set of join definitions.
6247
- *
6248
- * @param {Record<string, JoinDefinition>} joins - An object where keys are table names
6249
- * and values are definitions of join conditions.
6250
- * @return {string} The constructed SQL join clause as a string.
6251
- */
6252
6635
  buildJoinClause(joins) {
6253
6636
  let joinSql = "";
6254
6637
  for (const [table, join] of Object.entries(joins)) {
6255
- joinSql += ` LEFT JOIN ${(0, import_lodash_es.snakeCase)(table)} ${join.alias} ON ${join.on}`;
6638
+ const alias = join.alias ? ` ${join.alias}` : "";
6639
+ joinSql += ` LEFT JOIN ${(0, import_lodash_es.snakeCase)(table)}${alias} ON ${join.on}`;
6256
6640
  if (join.joins) joinSql += " " + this.buildJoinClause(join.joins);
6257
6641
  }
6258
6642
  return joinSql;
6259
6643
  }
6260
- /**
6261
- * Recursively resolves nested data structure by processing special operations and transforming the data accordingly.
6262
- * Handles specific object structures with sub-operations, strings with specific commands, and other nested objects.
6263
- *
6264
- * @param {any} data The initial data to be resolved, which can be an object, array, or primitive value.
6265
- * @param {string} tableName The name of the table associated with the data, used contextually for operation resolution.
6266
- * @return {Promise<any>} A promise that resolves to the fully processed data structure with all nested elements resolved.
6267
- */
6268
- async resolveNestedData(data, tableName) {
6269
- if (Array.isArray(data))
6270
- return Promise.all(data.map((d) => this.resolveNestedData(d, tableName)));
6271
- if (typeof data !== "object" || data === null) return data;
6644
+ async resolveNestedData(registration, data, tableName) {
6645
+ if (Array.isArray(data)) {
6646
+ return Promise.all(
6647
+ data.map((entry) => this.resolveNestedData(registration, entry, tableName))
6648
+ );
6649
+ }
6650
+ if (typeof data !== "object" || data === null) {
6651
+ return data;
6652
+ }
6272
6653
  const resolved = { ...data };
6273
6654
  for (const [key, value] of Object.entries(data)) {
6274
6655
  if (typeof value === "object" && value !== null && "subOperation" in value) {
6275
- const subOp = value;
6276
- resolved[key] = await this.executeSubOperation(subOp);
6656
+ const subOperation = value;
6657
+ resolved[key] = await this.executeSubOperation(registration, subOperation);
6277
6658
  } else if (typeof value === "string" && ["increment", "decrement", "set"].includes(value)) {
6278
6659
  resolved[key] = { __effect: value };
6279
- } else if (typeof value === "object") {
6280
- resolved[key] = await this.resolveNestedData(value, tableName);
6660
+ } else if (typeof value === "object" && value !== null) {
6661
+ resolved[key] = await this.resolveNestedData(registration, value, tableName);
6281
6662
  }
6282
6663
  }
6283
6664
  return resolved;
6284
6665
  }
6285
- /**
6286
- * Executes a sub-operation against the database, such as an insert or query operation.
6287
- *
6288
- * @param {SubOperation} op - The operation to be executed. Contains details such as the type of sub-operation
6289
- * (e.g., "insert" or "query"), the target table, data to be inserted, filters for querying, fields to be retrieved, etc.
6290
- * @return {Promise<any>} A promise that resolves with the result of the operation.
6291
- * For "insert", the result will include the inserted row or a partial response for uuid conflicts.
6292
- * For "query", the result will include the first row that matches the query condition. If no result is found,
6293
- * resolves with an empty object.
6294
- * @throws Throws an error if the operation fails. Rolls back the transaction in case of an error.
6295
- */
6296
- async executeSubOperation(op) {
6297
- const client = await this.getClient();
6298
- try {
6299
- await client.query("BEGIN");
6300
- let result;
6301
- if (op.subOperation === "insert") {
6302
- const resolvedData = await this.resolveNestedData(op.data, op.table);
6303
- const sql = `INSERT INTO ${op.table} (${Object.keys(resolvedData).map((k) => (0, import_lodash_es.snakeCase)(k)).join(", ")}) VALUES (${Object.values(resolvedData).map((_, i) => `$${i + 1}`).join(", ")}) ON CONFLICT DO NOTHING RETURNING ${op.return ?? "*"}`;
6304
- result = await client.query(sql, Object.values(resolvedData));
6305
- result = result.rows[0]?.[op.return ?? "uuid"];
6306
- if (!result) {
6307
- result = op.return && op.return in resolvedData ? resolvedData[op.return] : resolvedData["uuid"];
6308
- }
6309
- } else if (op.subOperation === "query") {
6310
- const params = [];
6311
- const whereClause = this.buildWhereClause(op.filter || {}, params);
6312
- const sql = `SELECT ${op.fields?.join(", ") || "*"} FROM ${op.table} ${whereClause} LIMIT 1`;
6313
- result = (await client.query(sql, params)).rows[0]?.[op.return ?? "uuid"];
6314
- }
6315
- await client.query("COMMIT");
6316
- return result || {};
6317
- } catch (error) {
6318
- await client.query("ROLLBACK");
6319
- throw error;
6320
- } finally {
6321
- client.release();
6666
+ async executeSubOperation(registration, operation) {
6667
+ const targetTableName = operation.table;
6668
+ if (!registration.schema.tables[targetTableName]) {
6669
+ throw new Error(
6670
+ `Sub-operation table '${targetTableName}' does not exist in actor schema`
6671
+ );
6322
6672
  }
6323
- }
6324
- /**
6325
- * Creates a database task configured for specific operations such as query, insert, update, or delete on a given table.
6326
- *
6327
- * @param {DbOperationType} op - The type of database operation to perform (e.g., "query", "insert", "update", "delete").
6328
- * @param {string} tableName - The name of the table on which the operation will be performed.
6329
- * @param {TableDefinition} table - The table definition that includes configurations such as custom signal triggers and emissions.
6330
- * @param {function(string, AnyObject): Promise<any>} queryFunction - The function to execute the database operation. It takes the table name and a context object as arguments and returns a promise.
6331
- * @param {ServerOptions} options - The options for configuring the server context and metadata behavior.
6332
- * @return {void} This function does not return a value, but it registers a database task for the specified operation.
6333
- */
6334
- createDatabaseTask(op, tableName, table, queryFunction, options) {
6335
- const opAction = op === "query" ? "queried" : op === "insert" ? "inserted" : op === "update" ? "updated" : op === "delete" ? "deleted" : "";
6336
- const defaultSignal = `global.${options.isMeta ? "meta." : ""}${tableName}.${opAction}`;
6337
- const taskName = `${op.charAt(0).toUpperCase() + op.slice(1)} ${tableName}`;
6338
- const schema = this.getInputSchema(op, tableName, table);
6339
- const task = CadenzaService.createThrottledTask(
6340
- taskName,
6341
- async (context, emit) => {
6342
- for (const action of Object.keys(table.customSignals?.triggers ?? {})) {
6343
- const triggerConditions = (
6344
- // @ts-ignore
6345
- table.customSignals?.triggers?.[action].filter(
6346
- (trigger) => trigger.condition
6347
- )
6348
- );
6349
- for (const triggerCondition of triggerConditions ?? []) {
6350
- if (triggerCondition.condition && !triggerCondition.condition(context)) {
6351
- return {
6352
- failed: true,
6353
- error: `Condition for signal trigger failed: ${triggerCondition.signal}`
6354
- };
6355
- }
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) {
6365
- context.queryData = {
6366
- ...context.queryData,
6367
- ...queryData
6368
- };
6369
- } else {
6370
- context = {
6371
- ...context,
6372
- ...queryData
6373
- };
6374
- }
6375
- }
6376
- }
6377
- try {
6378
- const result = await queryFunction(
6379
- tableName,
6380
- context.queryData ?? context
6381
- );
6382
- context = {
6383
- ...context,
6384
- ...result
6385
- };
6386
- } catch (e) {
6387
- CadenzaService.log(
6388
- "Database task errored.",
6389
- { taskName, error: e },
6390
- "error"
6391
- );
6392
- throw e;
6393
- }
6394
- if (!context.errored) {
6395
- for (const signal of table.customSignals?.emissions?.[op] ?? []) {
6396
- if (signal.condition && !signal.condition(context)) {
6397
- continue;
6398
- }
6399
- emit(signal.signal ?? signal, context);
6400
- }
6401
- }
6402
- if (tableName !== "system_log" && context.errored) {
6403
- CadenzaService.log(
6404
- `ERROR in ${taskName}`,
6405
- JSON.stringify({
6406
- data: context.data,
6407
- queryData: context.queryData,
6408
- filter: context.filter,
6409
- fields: context.fields,
6410
- joins: context.joins,
6411
- sort: context.sort,
6412
- limit: context.limit,
6413
- offset: context.offset,
6414
- error: context.__error
6415
- }),
6416
- "error"
6417
- );
6673
+ const pool = this.getPoolOrThrow(registration);
6674
+ const safetyPolicy = this.resolveSafetyPolicy(registration);
6675
+ return this.executeWithTransaction(pool, true, async (client) => {
6676
+ if (operation.subOperation === "insert") {
6677
+ const resolvedData = await this.resolveNestedData(
6678
+ registration,
6679
+ operation.data,
6680
+ operation.table
6681
+ );
6682
+ const row = ensurePlainObject(resolvedData, "sub-operation insert data");
6683
+ const keys = Object.keys(row);
6684
+ const params = Object.values(row);
6685
+ 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 ?? "*"}`;
6686
+ const result2 = await this.withTimeout(
6687
+ () => client.query(sql2, params),
6688
+ safetyPolicy.statementTimeoutMs,
6689
+ `Sub-operation insert timeout on table ${operation.table}`
6690
+ );
6691
+ const returnKey2 = operation.return ?? "uuid";
6692
+ if (result2.rows[0]?.[returnKey2] !== void 0) {
6693
+ return result2.rows[0][returnKey2];
6418
6694
  }
6419
- delete context.queryData;
6420
- delete context.data;
6421
- delete context.filter;
6422
- delete context.fields;
6423
- delete context.joins;
6424
- delete context.sort;
6425
- delete context.limit;
6426
- delete context.offset;
6427
- return context;
6428
- },
6429
- (context) => context?.__metadata?.__executionTraceId ?? context?.__executionTraceId ?? "default",
6430
- `Auto-generated ${op} task for ${tableName}`,
6431
- {
6432
- isMeta: options.isMeta,
6433
- isSubMeta: options.isMeta,
6434
- validateInputContext: options.securityProfile !== "low",
6435
- inputSchema: schema
6695
+ return row[returnKey2] ?? row.uuid ?? {};
6436
6696
  }
6437
- ).doOn(
6438
- ...table.customSignals?.triggers?.[op]?.map((signal) => {
6439
- return typeof signal === "string" ? signal : signal.signal;
6440
- }) ?? []
6441
- ).emits(defaultSignal).attachSignal(
6442
- ...table.customSignals?.emissions?.[op]?.map((signal) => {
6443
- return typeof signal === "string" ? signal : signal.signal;
6444
- }) ?? []
6445
- );
6446
- if (op === "query") {
6447
- const { intents, warnings } = resolveTableQueryIntents(
6448
- CadenzaService.serviceRegistry?.serviceName,
6449
- tableName,
6450
- table,
6451
- schema
6697
+ const queryParams = [];
6698
+ const whereClause = this.buildWhereClause(operation.filter || {}, queryParams);
6699
+ const sql = `SELECT ${(operation.fields ?? ["*"]).map((field) => field === "*" ? field : (0, import_lodash_es.snakeCase)(field)).join(", ")} FROM ${operation.table} ${whereClause} LIMIT 1`;
6700
+ const result = await this.withTimeout(
6701
+ () => client.query(sql, queryParams),
6702
+ safetyPolicy.statementTimeoutMs,
6703
+ `Sub-operation query timeout on table ${operation.table}`
6452
6704
  );
6453
- for (const warning of warnings) {
6454
- CadenzaService.log(
6455
- "Skipped custom query intent registration.",
6456
- {
6457
- tableName,
6458
- warning
6459
- },
6460
- "warning"
6461
- );
6462
- }
6463
- for (const intent of intents) {
6464
- CadenzaService.defineIntent({
6465
- name: intent.name,
6466
- description: intent.description,
6467
- input: intent.input
6468
- });
6469
- }
6470
- task.respondsTo(...intents.map((intent) => intent.name));
6471
- }
6705
+ const returnKey = operation.return ?? "uuid";
6706
+ return result.rows[0]?.[returnKey] ?? {};
6707
+ });
6472
6708
  }
6473
6709
  getInputSchema(op, tableName, table) {
6474
6710
  const inputSchema = {
@@ -6487,66 +6723,43 @@ var DatabaseController = class _DatabaseController {
6487
6723
  }
6488
6724
  inputSchema.properties.transaction = getTransactionSchema();
6489
6725
  inputSchema.properties.queryData.properties.transaction = inputSchema.properties.transaction;
6490
- switch (op) {
6491
- case "insert":
6492
- inputSchema.properties.data = getInsertDataSchemaFromTable(
6493
- table,
6494
- tableName
6495
- );
6496
- inputSchema.properties.queryData.properties.data = inputSchema.properties.data;
6497
- inputSchema.properties.batch = getQueryBatchSchemaFromTable();
6498
- inputSchema.properties.queryData.properties.batch = inputSchema.properties.batch;
6499
- inputSchema.properties.onConflict = getQueryOnConflictSchemaFromTable(
6500
- table,
6501
- tableName
6502
- );
6503
- inputSchema.properties.queryData.properties.onConflict = inputSchema.properties.onConflict;
6504
- break;
6505
- case "query":
6506
- inputSchema.properties.filter = getQueryFilterSchemaFromTable(
6507
- table,
6508
- tableName
6509
- );
6510
- inputSchema.properties.queryData.properties.filter = inputSchema.properties.filter;
6511
- inputSchema.properties.fields = getQueryFieldsSchemaFromTable(
6512
- table,
6513
- tableName
6514
- );
6515
- inputSchema.properties.queryData.properties.fields = inputSchema.properties.fields;
6516
- inputSchema.properties.joins = getQueryJoinsSchemaFromTable(
6517
- table,
6518
- tableName
6519
- );
6520
- inputSchema.properties.queryData.properties.joins = inputSchema.properties.joins;
6521
- inputSchema.properties.sort = getQuerySortSchemaFromTable(
6522
- table,
6523
- tableName
6524
- );
6525
- inputSchema.properties.queryData.properties.sort = inputSchema.properties.sort;
6526
- inputSchema.properties.limit = getQueryLimitSchemaFromTable();
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;
6726
+ if (op === "insert" || op === "update") {
6727
+ inputSchema.properties.data = getInsertDataSchemaFromTable(table, tableName);
6728
+ inputSchema.properties.queryData.properties.data = inputSchema.properties.data;
6729
+ }
6730
+ if (op === "insert") {
6731
+ inputSchema.properties.batch = getQueryBatchSchemaFromTable();
6732
+ inputSchema.properties.queryData.properties.batch = inputSchema.properties.batch;
6733
+ inputSchema.properties.onConflict = getQueryOnConflictSchemaFromTable(
6734
+ table,
6735
+ tableName
6736
+ );
6737
+ inputSchema.properties.queryData.properties.onConflict = inputSchema.properties.onConflict;
6738
+ }
6739
+ if (op === "query" || op === "update" || op === "delete") {
6740
+ inputSchema.properties.filter = getQueryFilterSchemaFromTable(table, tableName);
6741
+ inputSchema.properties.queryData.properties.filter = inputSchema.properties.filter;
6742
+ }
6743
+ if (op === "query") {
6744
+ inputSchema.properties.queryMode = getQueryModeSchema();
6745
+ inputSchema.properties.queryData.properties.queryMode = inputSchema.properties.queryMode;
6746
+ inputSchema.properties.fields = getQueryFieldsSchemaFromTable(table, tableName);
6747
+ inputSchema.properties.queryData.properties.fields = inputSchema.properties.fields;
6748
+ inputSchema.properties.joins = getQueryJoinsSchemaFromTable(table, tableName);
6749
+ inputSchema.properties.queryData.properties.joins = inputSchema.properties.joins;
6750
+ inputSchema.properties.sort = getQuerySortSchemaFromTable(table, tableName);
6751
+ inputSchema.properties.queryData.properties.sort = inputSchema.properties.sort;
6752
+ inputSchema.properties.aggregates = getQueryAggregatesSchemaFromTable(
6753
+ table,
6754
+ tableName
6755
+ );
6756
+ inputSchema.properties.queryData.properties.aggregates = inputSchema.properties.aggregates;
6757
+ inputSchema.properties.groupBy = getQueryGroupBySchemaFromTable(table, tableName);
6758
+ inputSchema.properties.queryData.properties.groupBy = inputSchema.properties.groupBy;
6759
+ inputSchema.properties.limit = getQueryLimitSchemaFromTable();
6760
+ inputSchema.properties.queryData.properties.limit = inputSchema.properties.limit;
6761
+ inputSchema.properties.offset = getQueryOffsetSchemaFromTable();
6762
+ inputSchema.properties.queryData.properties.offset = inputSchema.properties.offset;
6550
6763
  }
6551
6764
  return inputSchema;
6552
6765
  }
@@ -6556,13 +6769,13 @@ function getInsertDataSchemaFromTable(table, tableName) {
6556
6769
  type: "object",
6557
6770
  properties: {
6558
6771
  ...Object.fromEntries(
6559
- Object.entries(table.fields).map((field) => {
6772
+ Object.entries(table.fields).map(([fieldName, field]) => {
6560
6773
  return [
6561
- field[0],
6774
+ fieldName,
6562
6775
  {
6563
6776
  value: {
6564
- type: tableFieldTypeToSchemaType(field[1].type),
6565
- description: `Inferred from field '${field[0]}' of type [${field[1].type}] on table ${tableName}.`
6777
+ type: tableFieldTypeToSchemaType(field.type),
6778
+ description: `Inferred from field '${fieldName}' of type [${field.type}] on table ${tableName}.`
6566
6779
  },
6567
6780
  effect: {
6568
6781
  type: "string",
@@ -6611,7 +6824,7 @@ function getInsertDataSchemaFromTable(table, tableName) {
6611
6824
  })
6612
6825
  )
6613
6826
  },
6614
- required: Object.entries(table.fields).filter((field) => field[1].required || field[1].primary).map((field) => field[0]),
6827
+ required: Object.entries(table.fields).filter(([, field]) => field.required || field.primary).map(([fieldName]) => fieldName),
6615
6828
  strict: true
6616
6829
  };
6617
6830
  return {
@@ -6627,18 +6840,18 @@ function getQueryFilterSchemaFromTable(table, tableName) {
6627
6840
  type: "object",
6628
6841
  properties: {
6629
6842
  ...Object.fromEntries(
6630
- Object.entries(table.fields).map((field) => {
6843
+ Object.entries(table.fields).map(([fieldName, field]) => {
6631
6844
  return [
6632
- field[0],
6845
+ fieldName,
6633
6846
  {
6634
6847
  value: {
6635
- type: tableFieldTypeToSchemaType(field[1].type),
6636
- description: `Inferred from field '${field[0]}' of type [${field[1].type}] on table ${tableName}.`
6848
+ type: tableFieldTypeToSchemaType(field.type),
6849
+ description: `Inferred from field '${fieldName}' of type [${field.type}] on table ${tableName}.`
6637
6850
  },
6638
6851
  in: {
6639
6852
  type: "array",
6640
6853
  items: {
6641
- type: tableFieldTypeToSchemaType(field[1].type)
6854
+ type: tableFieldTypeToSchemaType(field.type)
6642
6855
  }
6643
6856
  }
6644
6857
  }
@@ -6647,7 +6860,7 @@ function getQueryFilterSchemaFromTable(table, tableName) {
6647
6860
  )
6648
6861
  },
6649
6862
  strict: true,
6650
- description: `Inferred from table '${tableName}' on database service ${CadenzaService.serviceRegistry?.serviceName ?? "unknown-service"}.`
6863
+ description: `Inferred from table '${tableName}' on postgres actor table contract.`
6651
6864
  };
6652
6865
  }
6653
6866
  function getQueryFieldsSchemaFromTable(table, tableName) {
@@ -6659,106 +6872,100 @@ function getQueryFieldsSchemaFromTable(table, tableName) {
6659
6872
  oneOf: Object.keys(table.fields)
6660
6873
  }
6661
6874
  },
6662
- description: `Inferred from table '${tableName}' on database service ${CadenzaService.serviceRegistry?.serviceName ?? "unknown-service"}.`
6875
+ description: `Inferred field projection from table '${tableName}'.`
6663
6876
  };
6664
6877
  }
6665
- function getQueryJoinsSchemaFromTable(table, tableName) {
6878
+ function getQueryModeSchema() {
6666
6879
  return {
6667
- type: "object",
6668
- properties: {
6669
- ...Object.fromEntries(
6670
- Object.entries(table.fields).map((field) => {
6671
- return [
6672
- field[0],
6673
- {
6674
- type: "object",
6675
- properties: {
6676
- on: {
6677
- type: "string"
6678
- },
6679
- fields: {
6680
- type: "array",
6681
- items: {
6682
- type: "string"
6683
- }
6684
- },
6685
- filter: {
6686
- type: "object"
6687
- },
6688
- returnAs: {
6689
- type: "string",
6690
- constraints: {
6691
- oneOf: ["array", "object"]
6692
- }
6693
- },
6694
- alias: {
6695
- type: "string"
6696
- },
6697
- joins: {
6698
- type: "object"
6699
- }
6700
- },
6701
- required: ["on", "fields"],
6702
- strict: true
6703
- }
6704
- ];
6705
- })
6706
- )
6880
+ type: "string",
6881
+ constraints: {
6882
+ oneOf: ["rows", "count", "exists", "one", "aggregate"]
6883
+ }
6884
+ };
6885
+ }
6886
+ function getQueryAggregatesSchemaFromTable(table, tableName) {
6887
+ return {
6888
+ type: "array",
6889
+ items: {
6890
+ type: "object",
6891
+ properties: {
6892
+ fn: {
6893
+ type: "string",
6894
+ constraints: {
6895
+ oneOf: ["count", "sum", "avg", "min", "max"]
6896
+ }
6897
+ },
6898
+ field: {
6899
+ type: "string",
6900
+ constraints: {
6901
+ oneOf: Object.keys(table.fields)
6902
+ }
6903
+ },
6904
+ as: {
6905
+ type: "string"
6906
+ },
6907
+ distinct: {
6908
+ type: "boolean"
6909
+ }
6910
+ },
6911
+ required: ["fn"],
6912
+ strict: true
6707
6913
  },
6708
- strict: true,
6709
- description: `Inferred from table '${tableName}' on database service ${CadenzaService.serviceRegistry?.serviceName ?? "unknown-service"}.`
6914
+ description: `Aggregate definitions inferred from table '${tableName}'.`
6710
6915
  };
6711
6916
  }
6712
- function getQuerySortSchemaFromTable(table, tableName) {
6917
+ function getQueryGroupBySchemaFromTable(table, tableName) {
6713
6918
  return {
6714
- type: "object",
6715
- properties: {
6716
- ...Object.fromEntries(
6717
- Object.entries(table.fields).map((field) => {
6718
- return [
6719
- field[0],
6720
- {
6721
- type: "string",
6722
- constraints: {
6723
- oneOf: ["asc", "desc"]
6724
- }
6725
- }
6726
- ];
6727
- })
6728
- )
6919
+ type: "array",
6920
+ items: {
6921
+ type: "string",
6922
+ constraints: {
6923
+ oneOf: Object.keys(table.fields)
6924
+ }
6729
6925
  },
6730
- strict: true,
6731
- description: `Inferred from table '${tableName}' on database service ${CadenzaService.serviceRegistry?.serviceName ?? "unknown-service"}.`
6926
+ description: `Group by fields inferred from table '${tableName}'.`
6927
+ };
6928
+ }
6929
+ function getQueryJoinsSchemaFromTable(_table, tableName) {
6930
+ return {
6931
+ type: "object",
6932
+ description: `Join definitions for table '${tableName}'.`
6933
+ };
6934
+ }
6935
+ function getQuerySortSchemaFromTable(_table, tableName) {
6936
+ return {
6937
+ type: "object",
6938
+ strict: false,
6939
+ description: `Sort definition for table '${tableName}'. Keys are validated at runtime against allowlists and aggregate aliases.`
6732
6940
  };
6733
6941
  }
6734
6942
  function getQueryLimitSchemaFromTable() {
6735
6943
  return {
6736
6944
  type: "number",
6737
6945
  constraints: {
6738
- min: 1
6739
- },
6740
- description: "Limit for query results"
6946
+ min: 0,
6947
+ max: 1e3
6948
+ }
6741
6949
  };
6742
6950
  }
6743
6951
  function getQueryOffsetSchemaFromTable() {
6744
6952
  return {
6745
6953
  type: "number",
6746
6954
  constraints: {
6747
- min: 0
6748
- },
6749
- description: "Offset for query results"
6955
+ min: 0,
6956
+ max: 1e6
6957
+ }
6750
6958
  };
6751
6959
  }
6752
- function getTransactionSchema() {
6960
+ function getQueryBatchSchemaFromTable() {
6753
6961
  return {
6754
- type: "boolean",
6755
- description: "Whether to run the query in a transaction"
6962
+ type: "boolean"
6756
6963
  };
6757
6964
  }
6758
- function getQueryBatchSchemaFromTable() {
6965
+ function getTransactionSchema() {
6759
6966
  return {
6760
6967
  type: "boolean",
6761
- description: "Whether to run the query in batch mode"
6968
+ description: "Execute the operation in a transaction."
6762
6969
  };
6763
6970
  }
6764
6971
  function getQueryOnConflictSchemaFromTable(table, tableName) {
@@ -6784,55 +6991,43 @@ function getQueryOnConflictSchemaFromTable(table, tableName) {
6784
6991
  }
6785
6992
  },
6786
6993
  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
- }
6994
+ type: "object"
6801
6995
  },
6802
6996
  where: {
6803
6997
  type: "string"
6804
6998
  }
6805
- },
6806
- required: ["do"]
6999
+ }
6807
7000
  }
6808
7001
  },
6809
- required: ["target", "action"],
6810
- strict: true
7002
+ strict: true,
7003
+ description: `Conflict strategy for inserts on table '${tableName}'.`
6811
7004
  };
6812
7005
  }
6813
7006
  function tableFieldTypeToSchemaType(type) {
6814
7007
  switch (type) {
6815
7008
  case "varchar":
6816
7009
  case "text":
6817
- case "jsonb":
6818
7010
  case "uuid":
7011
+ case "timestamp":
6819
7012
  case "date":
6820
7013
  case "geo_point":
6821
- case "bytea":
6822
7014
  return "string";
6823
7015
  case "int":
6824
7016
  case "bigint":
6825
7017
  case "decimal":
6826
- case "timestamp":
6827
7018
  return "number";
6828
7019
  case "boolean":
6829
7020
  return "boolean";
6830
7021
  case "array":
6831
7022
  return "array";
6832
7023
  case "object":
7024
+ case "jsonb":
6833
7025
  return "object";
7026
+ case "bytea":
7027
+ return "string";
7028
+ default:
7029
+ return "any";
6834
7030
  }
6835
- return "any";
6836
7031
  }
6837
7032
 
6838
7033
  // src/Cadenza.ts
@@ -8408,16 +8603,16 @@ var CadenzaService = class {
8408
8603
  this.createCadenzaService(serviceName, description, options);
8409
8604
  }
8410
8605
  /**
8411
- * Creates and initializes a database service with the provided name, schema, and configuration options.
8412
- * This method is not supported in a browser environment and will log a warning if called in such an environment.
8606
+ * Creates and initializes a PostgresActor-backed database service.
8607
+ * This is the canonical API for schema-driven postgres setup in cadenza-service.
8413
8608
  *
8414
- * @param {string} name - The name of the database service to be created.
8415
- * @param {DatabaseSchemaDefinition} schema - The schema definition for the database service.
8416
- * @param {string} [description=""] - An optional description of the database service.
8417
- * @param {ServerOptions & DatabaseOptions} [options={}] - Optional configuration settings for the database and server.
8418
- * @return {void} This method does not return a value.
8609
+ * @param {string} name - Logical actor/service name.
8610
+ * @param {DatabaseSchemaDefinition} schema - Database schema definition.
8611
+ * @param {string} [description=""] - Optional human-readable description.
8612
+ * @param {ServerOptions & DatabaseOptions} [options={}] - Server/database runtime options.
8613
+ * @return {void}
8419
8614
  */
8420
- static createDatabaseService(name, schema, description = "", options = {}) {
8615
+ static createPostgresActor(name, schema, description = "", options = {}) {
8421
8616
  if (isBrowser) {
8422
8617
  console.warn(
8423
8618
  "Database service creation is not supported in the browser. Use the CadenzaDB service instead."
@@ -8427,7 +8622,7 @@ var CadenzaService = class {
8427
8622
  if (this.serviceCreated) return;
8428
8623
  this.bootstrap();
8429
8624
  this.serviceRegistry.serviceName = name;
8430
- DatabaseController.instance;
8625
+ const databaseController = DatabaseController.instance;
8431
8626
  options = {
8432
8627
  loadBalance: true,
8433
8628
  useSocket: true,
@@ -8448,14 +8643,23 @@ var CadenzaService = class {
8448
8643
  isDatabase: true,
8449
8644
  ...options
8450
8645
  };
8451
- console.log("Creating database service", options);
8646
+ const registration = databaseController.createPostgresActor(
8647
+ name,
8648
+ schema,
8649
+ options
8650
+ );
8651
+ console.log("Creating PostgresActor", {
8652
+ serviceName: name,
8653
+ actorName: registration.actorName,
8654
+ options
8655
+ });
8452
8656
  this.emit("meta.database_init_requested", {
8453
8657
  schema,
8454
8658
  databaseName: options.databaseName,
8455
8659
  options
8456
8660
  });
8457
8661
  this.createMetaTask("Set database connection", () => {
8458
- this.createMetaTask("Insert database service", (_, emit) => {
8662
+ this.createMetaTask("Insert database service bridge", (_, emit) => {
8459
8663
  emit("global.meta.created_database_service", {
8460
8664
  data: {
8461
8665
  service_name: name,
@@ -8466,12 +8670,33 @@ var CadenzaService = class {
8466
8670
  });
8467
8671
  this.log("Database service created", {
8468
8672
  name,
8469
- isMeta: options.isMeta
8673
+ isMeta: options.isMeta,
8674
+ actorName: registration.actorName
8470
8675
  });
8471
8676
  }).doOn("meta.service_registry.service_inserted");
8472
8677
  this.createCadenzaService(name, description, options);
8473
8678
  }).doOn("meta.database.setup_done");
8474
8679
  }
8680
+ /**
8681
+ * Creates a meta PostgresActor service.
8682
+ *
8683
+ * @param {string} name - Logical actor/service name.
8684
+ * @param {DatabaseSchemaDefinition} schema - Database schema definition.
8685
+ * @param {string} [description=""] - Optional description.
8686
+ * @param {ServerOptions & DatabaseOptions} [options={}] - Optional server/database options.
8687
+ * @return {void}
8688
+ */
8689
+ static createMetaPostgresActor(name, schema, description = "", options = {}) {
8690
+ this.bootstrap();
8691
+ options.isMeta = true;
8692
+ this.createPostgresActor(name, schema, description, options);
8693
+ }
8694
+ /**
8695
+ * Legacy compatibility wrapper. Prefer {@link createPostgresActor}.
8696
+ */
8697
+ static createDatabaseService(name, schema, description = "", options = {}) {
8698
+ this.createPostgresActor(name, schema, description, options);
8699
+ }
8475
8700
  /**
8476
8701
  * Creates a meta database service with the specified configuration.
8477
8702
  *
@@ -8482,9 +8707,7 @@ var CadenzaService = class {
8482
8707
  * @return {void} - This method does not return a value.
8483
8708
  */
8484
8709
  static createMetaDatabaseService(name, schema, description = "", options = {}) {
8485
- this.bootstrap();
8486
- options.isMeta = true;
8487
- this.createDatabaseService(name, schema, description, options);
8710
+ this.createMetaPostgresActor(name, schema, description, options);
8488
8711
  }
8489
8712
  static createActor(spec, options = {}) {
8490
8713
  this.bootstrap();