@restura/core 1.10.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -10,245 +10,52 @@ var __decorateClass = (decorators, target, key, kind) => {
10
10
  };
11
11
 
12
12
  // src/logger/logger.ts
13
- import { config } from "@restura/internal";
14
- import pino from "pino";
15
- import pinoPretty from "pino-pretty";
16
-
17
- // src/restura/RsError.ts
18
- var HtmlStatusCodes = /* @__PURE__ */ ((HtmlStatusCodes2) => {
19
- HtmlStatusCodes2[HtmlStatusCodes2["BAD_REQUEST"] = 400] = "BAD_REQUEST";
20
- HtmlStatusCodes2[HtmlStatusCodes2["UNAUTHORIZED"] = 401] = "UNAUTHORIZED";
21
- HtmlStatusCodes2[HtmlStatusCodes2["PAYMENT_REQUIRED"] = 402] = "PAYMENT_REQUIRED";
22
- HtmlStatusCodes2[HtmlStatusCodes2["FORBIDDEN"] = 403] = "FORBIDDEN";
23
- HtmlStatusCodes2[HtmlStatusCodes2["NOT_FOUND"] = 404] = "NOT_FOUND";
24
- HtmlStatusCodes2[HtmlStatusCodes2["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
25
- HtmlStatusCodes2[HtmlStatusCodes2["REQUEST_TIMEOUT"] = 408] = "REQUEST_TIMEOUT";
26
- HtmlStatusCodes2[HtmlStatusCodes2["CONFLICT"] = 409] = "CONFLICT";
27
- HtmlStatusCodes2[HtmlStatusCodes2["GONE"] = 410] = "GONE";
28
- HtmlStatusCodes2[HtmlStatusCodes2["PAYLOAD_TOO_LARGE"] = 413] = "PAYLOAD_TOO_LARGE";
29
- HtmlStatusCodes2[HtmlStatusCodes2["UNSUPPORTED_MEDIA_TYPE"] = 415] = "UNSUPPORTED_MEDIA_TYPE";
30
- HtmlStatusCodes2[HtmlStatusCodes2["UPGRADE_REQUIRED"] = 426] = "UPGRADE_REQUIRED";
31
- HtmlStatusCodes2[HtmlStatusCodes2["UNPROCESSABLE_ENTITY"] = 422] = "UNPROCESSABLE_ENTITY";
32
- HtmlStatusCodes2[HtmlStatusCodes2["TOO_MANY_REQUESTS"] = 429] = "TOO_MANY_REQUESTS";
33
- HtmlStatusCodes2[HtmlStatusCodes2["SERVER_ERROR"] = 500] = "SERVER_ERROR";
34
- HtmlStatusCodes2[HtmlStatusCodes2["NOT_IMPLEMENTED"] = 501] = "NOT_IMPLEMENTED";
35
- HtmlStatusCodes2[HtmlStatusCodes2["BAD_GATEWAY"] = 502] = "BAD_GATEWAY";
36
- HtmlStatusCodes2[HtmlStatusCodes2["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE";
37
- HtmlStatusCodes2[HtmlStatusCodes2["GATEWAY_TIMEOUT"] = 504] = "GATEWAY_TIMEOUT";
38
- HtmlStatusCodes2[HtmlStatusCodes2["NETWORK_CONNECT_TIMEOUT"] = 599] = "NETWORK_CONNECT_TIMEOUT";
39
- return HtmlStatusCodes2;
40
- })(HtmlStatusCodes || {});
41
- var RsError = class _RsError extends Error {
42
- err;
43
- msg;
44
- options;
45
- status;
46
- constructor(errCode, message, options) {
47
- super(message);
48
- this.name = "RsError";
49
- this.err = errCode;
50
- this.msg = message || "";
51
- this.status = _RsError.htmlStatus(errCode);
52
- this.options = options;
53
- }
54
- toJSON() {
55
- return {
56
- type: this.name,
57
- err: this.err,
58
- message: this.message,
59
- msg: this.msg,
60
- status: this.status ?? 500,
61
- stack: this.stack ?? "",
62
- options: this.options
63
- };
64
- }
65
- static htmlStatus(code) {
66
- return htmlStatusMap[code];
67
- }
68
- static isRsError(error) {
69
- return error instanceof _RsError;
70
- }
71
- };
72
- var htmlStatusMap = {
73
- // 1:1 mappings to HTTP status codes
74
- BAD_REQUEST: 400 /* BAD_REQUEST */,
75
- UNAUTHORIZED: 401 /* UNAUTHORIZED */,
76
- PAYMENT_REQUIRED: 402 /* PAYMENT_REQUIRED */,
77
- FORBIDDEN: 403 /* FORBIDDEN */,
78
- NOT_FOUND: 404 /* NOT_FOUND */,
79
- METHOD_NOT_ALLOWED: 405 /* METHOD_NOT_ALLOWED */,
80
- REQUEST_TIMEOUT: 408 /* REQUEST_TIMEOUT */,
81
- CONFLICT: 409 /* CONFLICT */,
82
- GONE: 410 /* GONE */,
83
- PAYLOAD_TOO_LARGE: 413 /* PAYLOAD_TOO_LARGE */,
84
- UNSUPPORTED_MEDIA_TYPE: 415 /* UNSUPPORTED_MEDIA_TYPE */,
85
- UPGRADE_REQUIRED: 426 /* UPGRADE_REQUIRED */,
86
- UNPROCESSABLE_ENTITY: 422 /* UNPROCESSABLE_ENTITY */,
87
- TOO_MANY_REQUESTS: 429 /* TOO_MANY_REQUESTS */,
88
- SERVER_ERROR: 500 /* SERVER_ERROR */,
89
- NOT_IMPLEMENTED: 501 /* NOT_IMPLEMENTED */,
90
- BAD_GATEWAY: 502 /* BAD_GATEWAY */,
91
- SERVICE_UNAVAILABLE: 503 /* SERVICE_UNAVAILABLE */,
92
- GATEWAY_TIMEOUT: 504 /* GATEWAY_TIMEOUT */,
93
- NETWORK_CONNECT_TIMEOUT: 599 /* NETWORK_CONNECT_TIMEOUT */,
94
- // Specific business errors mapped to appropriate HTTP codes
95
- UNKNOWN_ERROR: 500 /* SERVER_ERROR */,
96
- RATE_LIMIT_EXCEEDED: 429 /* TOO_MANY_REQUESTS */,
97
- INVALID_TOKEN: 401 /* UNAUTHORIZED */,
98
- INCORRECT_EMAIL_OR_PASSWORD: 401 /* UNAUTHORIZED */,
99
- DUPLICATE: 409 /* CONFLICT */,
100
- CONNECTION_ERROR: 504 /* GATEWAY_TIMEOUT */,
101
- SCHEMA_ERROR: 500 /* SERVER_ERROR */,
102
- DATABASE_ERROR: 500 /* SERVER_ERROR */
103
- };
104
-
105
- // src/logger/loggerConfigSchema.ts
106
- import { z } from "zod";
107
- var loggerConfigSchema = z.object({
108
- level: z.enum(["fatal", "error", "warn", "info", "debug", "silly", "trace"]).default("info"),
109
- transports: z.array(z.custom()).optional(),
110
- serializers: z.object({
111
- err: z.custom().optional()
112
- }).optional(),
113
- stream: z.custom().optional()
114
- }).refine((data) => !(data.transports && data.stream), {
115
- message: "You must provide either a transports array or a stream object, but not both",
116
- path: ["transports"]
117
- });
118
-
119
- // src/logger/logger.ts
120
- var loggerConfig = await config.validate("logger", loggerConfigSchema);
121
- var logLevelMap = {
122
- fatal: "fatal",
123
- error: "error",
124
- warn: "warn",
125
- info: "info",
126
- debug: "debug",
127
- silly: "trace",
128
- trace: "trace"
129
- };
130
- var currentLogLevel = logLevelMap[loggerConfig.level];
131
- var defaultStream = pinoPretty({
132
- colorize: true,
133
- translateTime: "yyyy-mm-dd HH:MM:ss.l",
134
- ignore: "pid,hostname,_meta",
135
- // _meta allows a user to pass in metadata for JSON but not print it to the console
136
- messageFormat: "{msg}",
137
- levelFirst: true,
138
- customColors: "error:red,warn:yellow,info:green,debug:blue,trace:magenta",
139
- destination: process.stdout
140
- });
141
- function isAxiosError(error) {
142
- const isObject = (error2) => error2 !== null && typeof error2 === "object";
143
- return isObject(error) && "isAxiosError" in error && error.isAxiosError === true;
144
- }
145
- var baseSerializer = pino.stdSerializers.err;
146
- var defaultSerializer = (error) => {
147
- if (isAxiosError(error)) {
148
- const err = error;
149
- return {
150
- type: "AxiosError",
151
- message: err.message,
152
- stack: err.stack,
153
- url: err.config?.url,
154
- method: err.config?.method?.toUpperCase(),
155
- status: err.response?.status,
156
- responseData: err.response?.data
157
- };
158
- }
159
- if (RsError.isRsError(error)) {
160
- return error.toJSON();
13
+ var LEVEL_ORDER = ["fatal", "error", "warn", "info", "debug", "trace"];
14
+ function isLevelEnabled(configured, requested) {
15
+ return LEVEL_ORDER.indexOf(requested) <= LEVEL_ORDER.indexOf(configured);
16
+ }
17
+ var envLevel = process.env.RESTURA_LOG_LEVEL;
18
+ var consoleLoggerLevel = LEVEL_ORDER.includes(envLevel) ? envLevel : "info";
19
+ var consoleLogger = {
20
+ level: consoleLoggerLevel,
21
+ fatal: (msg, ...args) => {
22
+ if (isLevelEnabled(consoleLoggerLevel, "fatal")) console.error(msg, ...args);
23
+ },
24
+ error: (msg, ...args) => {
25
+ if (isLevelEnabled(consoleLoggerLevel, "error")) console.error(msg, ...args);
26
+ },
27
+ warn: (msg, ...args) => {
28
+ if (isLevelEnabled(consoleLoggerLevel, "warn")) console.warn(msg, ...args);
29
+ },
30
+ info: (msg, ...args) => {
31
+ if (isLevelEnabled(consoleLoggerLevel, "info")) console.log(msg, ...args);
32
+ },
33
+ debug: (msg, ...args) => {
34
+ if (isLevelEnabled(consoleLoggerLevel, "debug")) console.debug(msg, ...args);
35
+ },
36
+ trace: (msg, ...args) => {
37
+ if (isLevelEnabled(consoleLoggerLevel, "trace")) console.debug(msg, ...args);
161
38
  }
162
- return baseSerializer(error);
163
39
  };
164
- var errorSerializer = (() => {
165
- try {
166
- return loggerConfig.serializers?.err ? loggerConfig.serializers.err(baseSerializer) : defaultSerializer;
167
- } catch (error) {
168
- console.error("Failed to initialize custom error serializer, falling back to default", error);
169
- return defaultSerializer;
170
- }
171
- })();
172
- var pinoLogger = pino(
173
- {
174
- level: currentLogLevel,
175
- ...loggerConfig.transports ? { transport: { targets: loggerConfig.transports } } : {},
176
- serializers: {
177
- err: errorSerializer
178
- }
40
+ var _impl = consoleLogger;
41
+ var logger = {
42
+ get level() {
43
+ return _impl.level;
179
44
  },
180
- loggerConfig.stream ? loggerConfig.stream : defaultStream
181
- );
182
- function buildContext(args) {
183
- const ctx = {};
184
- const prims = [];
185
- for (const arg of args) {
186
- if (arg instanceof Error && !ctx.err) {
187
- ctx.err = arg;
188
- } else if (arg && typeof arg === "object" && !Array.isArray(arg)) {
189
- Object.assign(ctx, arg);
190
- } else {
191
- prims.push(arg);
192
- }
45
+ fatal: (msg, ...args) => _impl.fatal(msg, ...args),
46
+ error: (msg, ...args) => _impl.error(msg, ...args),
47
+ warn: (msg, ...args) => _impl.warn(msg, ...args),
48
+ info: (msg, ...args) => _impl.info(msg, ...args),
49
+ debug: (msg, ...args) => _impl.debug(msg, ...args),
50
+ trace: (msg, ...args) => _impl.trace(msg, ...args)
51
+ };
52
+ function setLogger(impl) {
53
+ if (impl === logger) {
54
+ throw new Error("setLogger: cannot install the proxy logger as its own implementation");
193
55
  }
194
- if (prims.length) ctx.args = prims;
195
- return ctx;
56
+ if (impl === _impl) return;
57
+ _impl = impl;
196
58
  }
197
- function log(level, message, ...args) {
198
- pinoLogger[level](buildContext(args), message);
199
- }
200
- var logger = {
201
- level: loggerConfig.level,
202
- fatal: ((msg, ...args) => {
203
- if (typeof msg === "string") {
204
- log("fatal", msg, ...args);
205
- } else {
206
- pinoLogger.fatal(msg);
207
- }
208
- }),
209
- error: ((msg, ...args) => {
210
- if (typeof msg === "string") {
211
- log("error", msg, ...args);
212
- } else {
213
- pinoLogger.error(msg);
214
- }
215
- }),
216
- warn: ((msg, ...args) => {
217
- if (typeof msg === "string") {
218
- log("warn", msg, ...args);
219
- } else {
220
- pinoLogger.warn(msg);
221
- }
222
- }),
223
- info: ((msg, ...args) => {
224
- if (typeof msg === "string") {
225
- log("info", msg, ...args);
226
- } else {
227
- pinoLogger.info(msg);
228
- }
229
- }),
230
- debug: ((msg, ...args) => {
231
- if (typeof msg === "string") {
232
- log("debug", msg, ...args);
233
- } else {
234
- pinoLogger.debug(msg);
235
- }
236
- }),
237
- silly: ((msg, ...args) => {
238
- if (typeof msg === "string") {
239
- log("trace", msg, ...args);
240
- } else {
241
- pinoLogger.trace(msg);
242
- }
243
- }),
244
- trace: ((msg, ...args) => {
245
- if (typeof msg === "string") {
246
- log("trace", msg, ...args);
247
- } else {
248
- pinoLogger.trace(msg);
249
- }
250
- })
251
- };
252
59
 
253
60
  // src/restura/eventManager.ts
254
61
  import Bluebird from "bluebird";
@@ -423,6 +230,94 @@ var SqlUtils = class _SqlUtils {
423
230
  }
424
231
  };
425
232
 
233
+ // src/restura/RsError.ts
234
+ var HtmlStatusCodes = /* @__PURE__ */ ((HtmlStatusCodes2) => {
235
+ HtmlStatusCodes2[HtmlStatusCodes2["BAD_REQUEST"] = 400] = "BAD_REQUEST";
236
+ HtmlStatusCodes2[HtmlStatusCodes2["UNAUTHORIZED"] = 401] = "UNAUTHORIZED";
237
+ HtmlStatusCodes2[HtmlStatusCodes2["PAYMENT_REQUIRED"] = 402] = "PAYMENT_REQUIRED";
238
+ HtmlStatusCodes2[HtmlStatusCodes2["FORBIDDEN"] = 403] = "FORBIDDEN";
239
+ HtmlStatusCodes2[HtmlStatusCodes2["NOT_FOUND"] = 404] = "NOT_FOUND";
240
+ HtmlStatusCodes2[HtmlStatusCodes2["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
241
+ HtmlStatusCodes2[HtmlStatusCodes2["REQUEST_TIMEOUT"] = 408] = "REQUEST_TIMEOUT";
242
+ HtmlStatusCodes2[HtmlStatusCodes2["CONFLICT"] = 409] = "CONFLICT";
243
+ HtmlStatusCodes2[HtmlStatusCodes2["GONE"] = 410] = "GONE";
244
+ HtmlStatusCodes2[HtmlStatusCodes2["PAYLOAD_TOO_LARGE"] = 413] = "PAYLOAD_TOO_LARGE";
245
+ HtmlStatusCodes2[HtmlStatusCodes2["UNSUPPORTED_MEDIA_TYPE"] = 415] = "UNSUPPORTED_MEDIA_TYPE";
246
+ HtmlStatusCodes2[HtmlStatusCodes2["UPGRADE_REQUIRED"] = 426] = "UPGRADE_REQUIRED";
247
+ HtmlStatusCodes2[HtmlStatusCodes2["UNPROCESSABLE_ENTITY"] = 422] = "UNPROCESSABLE_ENTITY";
248
+ HtmlStatusCodes2[HtmlStatusCodes2["TOO_MANY_REQUESTS"] = 429] = "TOO_MANY_REQUESTS";
249
+ HtmlStatusCodes2[HtmlStatusCodes2["SERVER_ERROR"] = 500] = "SERVER_ERROR";
250
+ HtmlStatusCodes2[HtmlStatusCodes2["NOT_IMPLEMENTED"] = 501] = "NOT_IMPLEMENTED";
251
+ HtmlStatusCodes2[HtmlStatusCodes2["BAD_GATEWAY"] = 502] = "BAD_GATEWAY";
252
+ HtmlStatusCodes2[HtmlStatusCodes2["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE";
253
+ HtmlStatusCodes2[HtmlStatusCodes2["GATEWAY_TIMEOUT"] = 504] = "GATEWAY_TIMEOUT";
254
+ HtmlStatusCodes2[HtmlStatusCodes2["NETWORK_CONNECT_TIMEOUT"] = 599] = "NETWORK_CONNECT_TIMEOUT";
255
+ return HtmlStatusCodes2;
256
+ })(HtmlStatusCodes || {});
257
+ var RsError = class _RsError extends Error {
258
+ err;
259
+ msg;
260
+ options;
261
+ status;
262
+ constructor(errCode, message, options) {
263
+ super(message);
264
+ this.name = "RsError";
265
+ this.err = errCode;
266
+ this.msg = message || "";
267
+ this.status = _RsError.htmlStatus(errCode);
268
+ this.options = options;
269
+ }
270
+ toJSON() {
271
+ return {
272
+ type: this.name,
273
+ err: this.err,
274
+ message: this.message,
275
+ msg: this.msg,
276
+ status: this.status ?? 500,
277
+ stack: this.stack ?? "",
278
+ options: this.options
279
+ };
280
+ }
281
+ static htmlStatus(code) {
282
+ return htmlStatusMap[code];
283
+ }
284
+ static isRsError(error) {
285
+ return error instanceof _RsError;
286
+ }
287
+ };
288
+ var htmlStatusMap = {
289
+ // 1:1 mappings to HTTP status codes
290
+ BAD_REQUEST: 400 /* BAD_REQUEST */,
291
+ UNAUTHORIZED: 401 /* UNAUTHORIZED */,
292
+ PAYMENT_REQUIRED: 402 /* PAYMENT_REQUIRED */,
293
+ FORBIDDEN: 403 /* FORBIDDEN */,
294
+ NOT_FOUND: 404 /* NOT_FOUND */,
295
+ METHOD_NOT_ALLOWED: 405 /* METHOD_NOT_ALLOWED */,
296
+ REQUEST_TIMEOUT: 408 /* REQUEST_TIMEOUT */,
297
+ CONFLICT: 409 /* CONFLICT */,
298
+ GONE: 410 /* GONE */,
299
+ PAYLOAD_TOO_LARGE: 413 /* PAYLOAD_TOO_LARGE */,
300
+ UNSUPPORTED_MEDIA_TYPE: 415 /* UNSUPPORTED_MEDIA_TYPE */,
301
+ UPGRADE_REQUIRED: 426 /* UPGRADE_REQUIRED */,
302
+ UNPROCESSABLE_ENTITY: 422 /* UNPROCESSABLE_ENTITY */,
303
+ TOO_MANY_REQUESTS: 429 /* TOO_MANY_REQUESTS */,
304
+ SERVER_ERROR: 500 /* SERVER_ERROR */,
305
+ NOT_IMPLEMENTED: 501 /* NOT_IMPLEMENTED */,
306
+ BAD_GATEWAY: 502 /* BAD_GATEWAY */,
307
+ SERVICE_UNAVAILABLE: 503 /* SERVICE_UNAVAILABLE */,
308
+ GATEWAY_TIMEOUT: 504 /* GATEWAY_TIMEOUT */,
309
+ NETWORK_CONNECT_TIMEOUT: 599 /* NETWORK_CONNECT_TIMEOUT */,
310
+ // Specific business errors mapped to appropriate HTTP codes
311
+ UNKNOWN_ERROR: 500 /* SERVER_ERROR */,
312
+ RATE_LIMIT_EXCEEDED: 429 /* TOO_MANY_REQUESTS */,
313
+ INVALID_TOKEN: 401 /* UNAUTHORIZED */,
314
+ INCORRECT_EMAIL_OR_PASSWORD: 401 /* UNAUTHORIZED */,
315
+ DUPLICATE: 409 /* CONFLICT */,
316
+ CONNECTION_ERROR: 504 /* GATEWAY_TIMEOUT */,
317
+ SCHEMA_ERROR: 500 /* SERVER_ERROR */,
318
+ DATABASE_ERROR: 500 /* SERVER_ERROR */
319
+ };
320
+
426
321
  // src/restura/validators/ResponseValidator.ts
427
322
  var ResponseValidator = class _ResponseValidator {
428
323
  rootMap;
@@ -905,7 +800,7 @@ declare namespace Restura {
905
800
 
906
801
  // src/restura/restura.ts
907
802
  import { ObjectUtils as ObjectUtils4, StringUtils as StringUtils3 } from "@redskytech/core-utils";
908
- import { config as config2 } from "@restura/internal";
803
+ import { config } from "@restura/internal";
909
804
 
910
805
  // ../../node_modules/.pnpm/autobind-decorator@2.4.0/node_modules/autobind-decorator/lib/esm/index.js
911
806
  function _typeof(obj) {
@@ -1259,12 +1154,12 @@ function customTypeValidationGenerator(currentSchema, ignoreGeneratedTypes = fal
1259
1154
  return type;
1260
1155
  });
1261
1156
  fs2.writeFileSync(temporaryFile.name, additionalImports + typesWithExport.join("\n"));
1262
- const config3 = {
1157
+ const config2 = {
1263
1158
  path: resolve(temporaryFile.name),
1264
1159
  tsconfig: path2.join(process.cwd(), "tsconfig.json"),
1265
1160
  skipTypeCheck: true
1266
1161
  };
1267
- const generator = createGenerator(config3);
1162
+ const generator = createGenerator(config2);
1268
1163
  customInterfaceNames.forEach((item) => {
1269
1164
  try {
1270
1165
  const ddlSchema = generator.createSchema(item);
@@ -1392,30 +1287,30 @@ var getMulterUpload = (directory) => {
1392
1287
  };
1393
1288
 
1394
1289
  // src/restura/schemas/resturaSchema.ts
1395
- import { z as z3 } from "zod";
1290
+ import { z as z2 } from "zod";
1396
1291
 
1397
1292
  // src/restura/schemas/validatorDataSchema.ts
1398
- import { z as z2 } from "zod";
1399
- var validatorDataSchemeValue = z2.union([z2.string(), z2.array(z2.string()), z2.number(), z2.array(z2.number())]);
1400
- var validatorDataSchema = z2.object({
1401
- type: z2.enum(["TYPE_CHECK", "MIN", "MAX", "ONE_OF"]),
1293
+ import { z } from "zod";
1294
+ var validatorDataSchemeValue = z.union([z.string(), z.array(z.string()), z.number(), z.array(z.number())]);
1295
+ var validatorDataSchema = z.object({
1296
+ type: z.enum(["TYPE_CHECK", "MIN", "MAX", "ONE_OF"]),
1402
1297
  value: validatorDataSchemeValue
1403
1298
  }).strict();
1404
1299
 
1405
1300
  // src/restura/schemas/resturaSchema.ts
1406
- var orderBySchema = z3.object({
1407
- columnName: z3.string(),
1408
- order: z3.enum(["ASC", "DESC"]),
1409
- tableName: z3.string()
1301
+ var orderBySchema = z2.object({
1302
+ columnName: z2.string(),
1303
+ order: z2.enum(["ASC", "DESC"]),
1304
+ tableName: z2.string()
1410
1305
  }).strict();
1411
- var groupBySchema = z3.object({
1412
- columnName: z3.string(),
1413
- tableName: z3.string()
1306
+ var groupBySchema = z2.object({
1307
+ columnName: z2.string(),
1308
+ tableName: z2.string()
1414
1309
  }).strict();
1415
- var whereDataSchema = z3.object({
1416
- tableName: z3.string().optional(),
1417
- columnName: z3.string().optional(),
1418
- operator: z3.enum([
1310
+ var whereDataSchema = z2.object({
1311
+ tableName: z2.string().optional(),
1312
+ columnName: z2.string().optional(),
1313
+ operator: z2.enum([
1419
1314
  "=",
1420
1315
  "<",
1421
1316
  ">",
@@ -1431,79 +1326,79 @@ var whereDataSchema = z3.object({
1431
1326
  "IS",
1432
1327
  "IS NOT"
1433
1328
  ]).optional(),
1434
- value: z3.string().or(z3.number()).optional(),
1435
- custom: z3.string().optional(),
1436
- conjunction: z3.enum(["AND", "OR"]).optional()
1329
+ value: z2.string().or(z2.number()).optional(),
1330
+ custom: z2.string().optional(),
1331
+ conjunction: z2.enum(["AND", "OR"]).optional()
1437
1332
  }).strict();
1438
- var assignmentDataSchema = z3.object({
1439
- name: z3.string(),
1440
- value: z3.string()
1333
+ var assignmentDataSchema = z2.object({
1334
+ name: z2.string(),
1335
+ value: z2.string()
1441
1336
  }).strict();
1442
- var joinDataSchema = z3.object({
1443
- table: z3.string(),
1444
- localTable: z3.string().optional(),
1337
+ var joinDataSchema = z2.object({
1338
+ table: z2.string(),
1339
+ localTable: z2.string().optional(),
1445
1340
  // Defaults to base table if not specificed
1446
- localTableAlias: z3.string().optional(),
1341
+ localTableAlias: z2.string().optional(),
1447
1342
  // If we are joining a table off of a previous join, this is the alias of the previous join
1448
- localColumnName: z3.string().optional(),
1449
- foreignColumnName: z3.string().optional(),
1450
- custom: z3.string().optional(),
1451
- type: z3.enum(["LEFT", "INNER", "RIGHT"]),
1452
- alias: z3.string()
1343
+ localColumnName: z2.string().optional(),
1344
+ foreignColumnName: z2.string().optional(),
1345
+ custom: z2.string().optional(),
1346
+ type: z2.enum(["LEFT", "INNER", "RIGHT"]),
1347
+ alias: z2.string()
1453
1348
  }).strict();
1454
- var requestDataSchema = z3.object({
1455
- name: z3.string(),
1456
- required: z3.boolean(),
1457
- isNullable: z3.boolean().optional(),
1458
- validator: z3.array(validatorDataSchema)
1349
+ var requestDataSchema = z2.object({
1350
+ name: z2.string(),
1351
+ required: z2.boolean(),
1352
+ isNullable: z2.boolean().optional(),
1353
+ validator: z2.array(validatorDataSchema)
1459
1354
  }).strict();
1460
- var responseDataSchema = z3.object({
1461
- name: z3.string(),
1462
- selector: z3.string().optional(),
1463
- subquery: z3.object({
1464
- table: z3.string(),
1465
- joins: z3.array(joinDataSchema),
1466
- where: z3.array(whereDataSchema),
1355
+ var responseDataSchema = z2.object({
1356
+ name: z2.string(),
1357
+ selector: z2.string().optional(),
1358
+ subquery: z2.object({
1359
+ table: z2.string(),
1360
+ joins: z2.array(joinDataSchema),
1361
+ where: z2.array(whereDataSchema),
1467
1362
  get properties() {
1468
- return z3.array(responseDataSchema);
1363
+ return z2.array(responseDataSchema);
1469
1364
  },
1470
1365
  groupBy: groupBySchema.optional(),
1471
1366
  orderBy: orderBySchema.optional()
1472
1367
  }).optional(),
1473
- type: z3.string().optional()
1368
+ type: z2.string().optional()
1474
1369
  // Type allows you to override the type of the response, used in custom selectors
1475
1370
  }).strict();
1476
- var routeDataBaseSchema = z3.object({
1477
- method: z3.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
1478
- name: z3.string(),
1479
- description: z3.string(),
1480
- path: z3.string(),
1481
- deprecation: z3.object({
1482
- date: z3.iso.datetime(),
1483
- message: z3.string().optional()
1371
+ var routeDataBaseSchema = z2.object({
1372
+ method: z2.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
1373
+ name: z2.string(),
1374
+ description: z2.string(),
1375
+ path: z2.string(),
1376
+ deprecation: z2.object({
1377
+ date: z2.iso.datetime(),
1378
+ message: z2.string().optional()
1484
1379
  }).optional(),
1485
- roles: z3.array(z3.string()),
1486
- scopes: z3.array(z3.string())
1380
+ roles: z2.array(z2.string()),
1381
+ scopes: z2.array(z2.string())
1487
1382
  }).strict();
1488
1383
  var standardRouteSchema = routeDataBaseSchema.extend({
1489
- type: z3.enum(["ONE", "ARRAY", "PAGED"]),
1490
- table: z3.string(),
1491
- joins: z3.array(joinDataSchema),
1492
- assignments: z3.array(assignmentDataSchema),
1493
- where: z3.array(whereDataSchema),
1494
- request: z3.array(requestDataSchema),
1495
- response: z3.array(responseDataSchema),
1384
+ type: z2.enum(["ONE", "ARRAY", "PAGED"]),
1385
+ table: z2.string(),
1386
+ joins: z2.array(joinDataSchema),
1387
+ assignments: z2.array(assignmentDataSchema),
1388
+ where: z2.array(whereDataSchema),
1389
+ request: z2.array(requestDataSchema),
1390
+ response: z2.array(responseDataSchema),
1496
1391
  groupBy: groupBySchema.optional(),
1497
1392
  orderBy: orderBySchema.optional()
1498
1393
  }).strict();
1499
1394
  var customRouteSchema = routeDataBaseSchema.extend({
1500
- type: z3.enum(["CUSTOM_ONE", "CUSTOM_ARRAY", "CUSTOM_PAGED"]),
1501
- responseType: z3.union([z3.string(), z3.enum(["string", "number", "boolean"])]),
1502
- requestType: z3.string().optional(),
1503
- request: z3.array(requestDataSchema).optional(),
1504
- fileUploadType: z3.enum(["SINGLE", "MULTIPLE"]).optional()
1395
+ type: z2.enum(["CUSTOM_ONE", "CUSTOM_ARRAY", "CUSTOM_PAGED"]),
1396
+ responseType: z2.union([z2.string(), z2.enum(["string", "number", "boolean"])]),
1397
+ requestType: z2.string().optional(),
1398
+ request: z2.array(requestDataSchema).optional(),
1399
+ fileUploadType: z2.enum(["SINGLE", "MULTIPLE"]).optional()
1505
1400
  }).strict();
1506
- var postgresColumnNumericTypesSchema = z3.enum([
1401
+ var postgresColumnNumericTypesSchema = z2.enum([
1507
1402
  "SMALLINT",
1508
1403
  // 2 bytes, -32,768 to 32,767
1509
1404
  "INTEGER",
@@ -1523,7 +1418,7 @@ var postgresColumnNumericTypesSchema = z3.enum([
1523
1418
  "BIGSERIAL"
1524
1419
  // auto-incrementing big integer
1525
1420
  ]);
1526
- var postgresColumnStringTypesSchema = z3.enum([
1421
+ var postgresColumnStringTypesSchema = z2.enum([
1527
1422
  "CHAR",
1528
1423
  // fixed-length, blank-padded
1529
1424
  "VARCHAR",
@@ -1533,7 +1428,7 @@ var postgresColumnStringTypesSchema = z3.enum([
1533
1428
  "BYTEA"
1534
1429
  // binary data
1535
1430
  ]);
1536
- var postgresColumnDateTypesSchema = z3.enum([
1431
+ var postgresColumnDateTypesSchema = z2.enum([
1537
1432
  "DATE",
1538
1433
  // calendar date (year, month, day)
1539
1434
  "TIMESTAMP",
@@ -1545,13 +1440,13 @@ var postgresColumnDateTypesSchema = z3.enum([
1545
1440
  "INTERVAL"
1546
1441
  // time span
1547
1442
  ]);
1548
- var postgresColumnJsonTypesSchema = z3.enum([
1443
+ var postgresColumnJsonTypesSchema = z2.enum([
1549
1444
  "JSON",
1550
1445
  // stores JSON data as raw text
1551
1446
  "JSONB"
1552
1447
  // stores JSON data in a binary format, optimized for query performance
1553
1448
  ]);
1554
- var mariaDbColumnNumericTypesSchema = z3.enum([
1449
+ var mariaDbColumnNumericTypesSchema = z2.enum([
1555
1450
  "BOOLEAN",
1556
1451
  // 1-byte A synonym for "TINYINT(1)". Supported from version 1.2.0 onwards.
1557
1452
  "TINYINT",
@@ -1571,7 +1466,7 @@ var mariaDbColumnNumericTypesSchema = z3.enum([
1571
1466
  "DOUBLE"
1572
1467
  // 8 bytes Stored in 64-bit IEEE-754 floating point format. As such, the number of significant digits is about 15 and the range of values is approximately +/-1e308.
1573
1468
  ]);
1574
- var mariaDbColumnStringTypesSchema = z3.enum([
1469
+ var mariaDbColumnStringTypesSchema = z2.enum([
1575
1470
  "CHAR",
1576
1471
  // 1, 2, 4, or 8 bytes Holds letters and special characters of fixed length. Max length is 255. Default and minimum size is 1 byte.
1577
1472
  "VARCHAR",
@@ -1597,7 +1492,7 @@ var mariaDbColumnStringTypesSchema = z3.enum([
1597
1492
  "ENUM"
1598
1493
  // Enum type
1599
1494
  ]);
1600
- var mariaDbColumnDateTypesSchema = z3.enum([
1495
+ var mariaDbColumnDateTypesSchema = z2.enum([
1601
1496
  "DATE",
1602
1497
  // 4-bytes Date has year, month, and day.
1603
1498
  "DATETIME",
@@ -1607,9 +1502,9 @@ var mariaDbColumnDateTypesSchema = z3.enum([
1607
1502
  "TIMESTAMP"
1608
1503
  // 4-bytes Values are stored as the number of seconds since 1970-01-01 00:00:00 UTC, and optionally microseconds.
1609
1504
  ]);
1610
- var columnDataSchema = z3.object({
1611
- name: z3.string(),
1612
- type: z3.union([
1505
+ var columnDataSchema = z2.object({
1506
+ name: z2.string(),
1507
+ type: z2.union([
1613
1508
  postgresColumnNumericTypesSchema,
1614
1509
  postgresColumnStringTypesSchema,
1615
1510
  postgresColumnDateTypesSchema,
@@ -1618,26 +1513,26 @@ var columnDataSchema = z3.object({
1618
1513
  mariaDbColumnStringTypesSchema,
1619
1514
  mariaDbColumnDateTypesSchema
1620
1515
  ]),
1621
- isNullable: z3.boolean(),
1622
- roles: z3.array(z3.string()),
1623
- scopes: z3.array(z3.string()),
1624
- comment: z3.string().optional(),
1625
- default: z3.string().optional(),
1626
- value: z3.string().optional(),
1627
- isPrimary: z3.boolean().optional(),
1628
- isUnique: z3.boolean().optional(),
1629
- hasAutoIncrement: z3.boolean().optional(),
1630
- length: z3.number().optional()
1516
+ isNullable: z2.boolean(),
1517
+ roles: z2.array(z2.string()),
1518
+ scopes: z2.array(z2.string()),
1519
+ comment: z2.string().optional(),
1520
+ default: z2.string().optional(),
1521
+ value: z2.string().optional(),
1522
+ isPrimary: z2.boolean().optional(),
1523
+ isUnique: z2.boolean().optional(),
1524
+ hasAutoIncrement: z2.boolean().optional(),
1525
+ length: z2.number().optional()
1631
1526
  }).strict();
1632
- var indexDataSchema = z3.object({
1633
- name: z3.string(),
1634
- columns: z3.array(z3.string()),
1635
- isUnique: z3.boolean(),
1636
- isPrimaryKey: z3.boolean(),
1637
- order: z3.enum(["ASC", "DESC"]),
1638
- where: z3.string().optional()
1527
+ var indexDataSchema = z2.object({
1528
+ name: z2.string(),
1529
+ columns: z2.array(z2.string()),
1530
+ isUnique: z2.boolean(),
1531
+ isPrimaryKey: z2.boolean(),
1532
+ order: z2.enum(["ASC", "DESC"]),
1533
+ where: z2.string().optional()
1639
1534
  }).strict();
1640
- var foreignKeyActionsSchema = z3.enum([
1535
+ var foreignKeyActionsSchema = z2.enum([
1641
1536
  "CASCADE",
1642
1537
  // CASCADE action for foreign keys
1643
1538
  "SET NULL",
@@ -1649,50 +1544,50 @@ var foreignKeyActionsSchema = z3.enum([
1649
1544
  "SET DEFAULT"
1650
1545
  // SET DEFAULT action for foreign keys
1651
1546
  ]);
1652
- var foreignKeyDataSchema = z3.object({
1653
- name: z3.string(),
1654
- column: z3.string(),
1655
- refTable: z3.string(),
1656
- refColumn: z3.string(),
1547
+ var foreignKeyDataSchema = z2.object({
1548
+ name: z2.string(),
1549
+ column: z2.string(),
1550
+ refTable: z2.string(),
1551
+ refColumn: z2.string(),
1657
1552
  onDelete: foreignKeyActionsSchema,
1658
1553
  onUpdate: foreignKeyActionsSchema
1659
1554
  }).strict();
1660
- var checkConstraintDataSchema = z3.object({
1661
- name: z3.string(),
1662
- check: z3.string()
1555
+ var checkConstraintDataSchema = z2.object({
1556
+ name: z2.string(),
1557
+ check: z2.string()
1663
1558
  }).strict();
1664
- var tableDataSchema = z3.object({
1665
- name: z3.string(),
1666
- columns: z3.array(columnDataSchema),
1667
- indexes: z3.array(indexDataSchema),
1668
- foreignKeys: z3.array(foreignKeyDataSchema),
1669
- checkConstraints: z3.array(checkConstraintDataSchema),
1670
- roles: z3.array(z3.string()),
1671
- scopes: z3.array(z3.string()),
1672
- notify: z3.union([z3.literal("ALL"), z3.array(z3.string())]).optional()
1559
+ var tableDataSchema = z2.object({
1560
+ name: z2.string(),
1561
+ columns: z2.array(columnDataSchema),
1562
+ indexes: z2.array(indexDataSchema),
1563
+ foreignKeys: z2.array(foreignKeyDataSchema),
1564
+ checkConstraints: z2.array(checkConstraintDataSchema),
1565
+ roles: z2.array(z2.string()),
1566
+ scopes: z2.array(z2.string()),
1567
+ notify: z2.union([z2.literal("ALL"), z2.array(z2.string())]).optional()
1673
1568
  }).strict();
1674
- var endpointDataSchema = z3.object({
1675
- name: z3.string(),
1676
- description: z3.string(),
1677
- baseUrl: z3.string(),
1678
- routes: z3.array(z3.union([standardRouteSchema, customRouteSchema]))
1569
+ var endpointDataSchema = z2.object({
1570
+ name: z2.string(),
1571
+ description: z2.string(),
1572
+ baseUrl: z2.string(),
1573
+ routes: z2.array(z2.union([standardRouteSchema, customRouteSchema]))
1679
1574
  }).strict();
1680
- var resturaSchema = z3.object({
1681
- database: z3.array(tableDataSchema),
1682
- endpoints: z3.array(endpointDataSchema),
1683
- globalParams: z3.array(z3.string()),
1684
- roles: z3.array(z3.string()),
1685
- scopes: z3.array(z3.string()),
1686
- customTypes: z3.array(z3.string())
1575
+ var resturaSchema = z2.object({
1576
+ database: z2.array(tableDataSchema),
1577
+ endpoints: z2.array(endpointDataSchema),
1578
+ globalParams: z2.array(z2.string()),
1579
+ roles: z2.array(z2.string()),
1580
+ scopes: z2.array(z2.string()),
1581
+ customTypes: z2.array(z2.string())
1687
1582
  }).strict();
1688
1583
  async function isSchemaValid(schemaToCheck) {
1689
1584
  try {
1690
1585
  resturaSchema.parse(schemaToCheck);
1691
1586
  return true;
1692
1587
  } catch (error) {
1693
- if (error instanceof z3.ZodError) {
1588
+ if (error instanceof z2.ZodError) {
1694
1589
  logger.error("Schema failed to validate with the following error:");
1695
- console.error(z3.prettifyError(error));
1590
+ console.error(z2.prettifyError(error));
1696
1591
  } else {
1697
1592
  logger.error(error);
1698
1593
  }
@@ -1903,33 +1798,23 @@ async function schemaValidation(req, res, next) {
1903
1798
  }
1904
1799
 
1905
1800
  // src/restura/schemas/resturaConfigSchema.ts
1906
- import { z as z4 } from "zod";
1801
+ import { z as z3 } from "zod";
1907
1802
  var isTsx = process.argv[1]?.endsWith(".ts");
1908
1803
  var isTsNode = process.env.TS_NODE_DEV || process.env.TS_NODE_PROJECT;
1909
1804
  var customApiFolderPath = isTsx || isTsNode ? "/src/api" : "/dist/api";
1910
- var resturaConfigSchema = z4.object({
1911
- authToken: z4.string().min(1, "Missing Restura Auth Token"),
1912
- sendErrorStackTrace: z4.boolean().default(false),
1913
- schemaFilePath: z4.string().default(process.cwd() + "/restura.schema.json"),
1914
- customApiFolderPath: z4.string().default(process.cwd() + customApiFolderPath),
1915
- generatedTypesPath: z4.string().default(process.cwd() + "/src/@types"),
1916
- fileTempCachePath: z4.string().optional(),
1917
- scratchDatabaseSuffix: z4.string().optional()
1805
+ var resturaConfigSchema = z3.object({
1806
+ authToken: z3.string().min(1, "Missing Restura Auth Token"),
1807
+ sendErrorStackTrace: z3.boolean().default(false),
1808
+ schemaFilePath: z3.string().default(process.cwd() + "/restura.schema.json"),
1809
+ customApiFolderPath: z3.string().default(process.cwd() + customApiFolderPath),
1810
+ generatedTypesPath: z3.string().default(process.cwd() + "/src/@types"),
1811
+ fileTempCachePath: z3.string().optional(),
1812
+ scratchDatabaseSuffix: z3.string().optional()
1918
1813
  });
1919
1814
 
1920
1815
  // src/restura/sql/PsqlEngine.ts
1921
1816
  import { ObjectUtils as ObjectUtils3 } from "@redskytech/core-utils";
1922
- import getDiff from "@wmfs/pg-diff-sync";
1923
- import pgInfo from "@wmfs/pg-info";
1924
- import pg2 from "pg";
1925
-
1926
- // src/restura/sql/PsqlPool.ts
1927
- import pg from "pg";
1928
-
1929
- // src/restura/sql/PsqlConnection.ts
1930
- import crypto from "crypto";
1931
- import { format as sqlFormat } from "sql-formatter";
1932
- import { z as z5 } from "zod";
1817
+ import pg3 from "pg";
1933
1818
 
1934
1819
  // src/restura/sql/PsqlUtils.ts
1935
1820
  import format from "pg-format";
@@ -2020,135 +1905,6 @@ function toSqlLiteral(value) {
2020
1905
  return format.literal(value);
2021
1906
  }
2022
1907
 
2023
- // src/restura/sql/PsqlConnection.ts
2024
- var PsqlConnection = class {
2025
- instanceId;
2026
- constructor(instanceId) {
2027
- this.instanceId = instanceId || crypto.randomUUID();
2028
- }
2029
- async queryOne(query, options, requesterDetails) {
2030
- const formattedQuery = questionMarksToOrderedParams(query);
2031
- const meta = { connectionInstanceId: this.instanceId, ...requesterDetails };
2032
- const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
2033
- `;
2034
- const startTime = process.hrtime();
2035
- try {
2036
- const response = await this.query(queryMetadata + formattedQuery, options);
2037
- if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
2038
- else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
2039
- this.logSqlStatement(formattedQuery, options, meta, startTime);
2040
- return response.rows[0];
2041
- } catch (error) {
2042
- this.logSqlStatement(formattedQuery, options, meta, startTime);
2043
- if (RsError.isRsError(error)) throw error;
2044
- if (error?.routine === "_bt_check_unique") {
2045
- throw new RsError("DUPLICATE", error.message);
2046
- }
2047
- throw new RsError("DATABASE_ERROR", `${error.message}`);
2048
- }
2049
- }
2050
- async queryOneSchema(query, params, requesterDetails, zodSchema) {
2051
- const result = await this.queryOne(query, params, requesterDetails);
2052
- try {
2053
- return zodSchema.parse(result);
2054
- } catch (error) {
2055
- if (error instanceof z5.ZodError) {
2056
- logger.error("Invalid data returned from database:");
2057
- logger.silly("\n" + JSON.stringify(result, null, 2));
2058
- logger.error("\n" + z5.prettifyError(error));
2059
- } else {
2060
- logger.error(error);
2061
- }
2062
- throw new RsError("DATABASE_ERROR", `Invalid data returned from database`);
2063
- }
2064
- }
2065
- async runQuery(query, options, requesterDetails) {
2066
- const formattedQuery = questionMarksToOrderedParams(query);
2067
- const meta = { connectionInstanceId: this.instanceId, ...requesterDetails };
2068
- const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
2069
- `;
2070
- const startTime = process.hrtime();
2071
- try {
2072
- const response = await this.query(queryMetadata + formattedQuery, options);
2073
- this.logSqlStatement(formattedQuery, options, meta, startTime);
2074
- return response.rows;
2075
- } catch (error) {
2076
- this.logSqlStatement(formattedQuery, options, meta, startTime);
2077
- if (error?.routine === "_bt_check_unique") {
2078
- throw new RsError("DUPLICATE", error.message);
2079
- }
2080
- throw new RsError("DATABASE_ERROR", `${error.message}`);
2081
- }
2082
- }
2083
- async runQuerySchema(query, params, requesterDetails, zodSchema) {
2084
- const result = await this.runQuery(query, params, requesterDetails);
2085
- try {
2086
- return z5.array(zodSchema).parse(result);
2087
- } catch (error) {
2088
- if (error instanceof z5.ZodError) {
2089
- logger.error("Invalid data returned from database:");
2090
- logger.silly("\n" + JSON.stringify(result, null, 2));
2091
- logger.error("\n" + z5.prettifyError(error));
2092
- } else {
2093
- logger.error(error);
2094
- }
2095
- throw new RsError("DATABASE_ERROR", `Invalid data returned from database`);
2096
- }
2097
- }
2098
- logSqlStatement(query, options, queryMetadata, startTime, prefix = "") {
2099
- if (logger.level !== "trace" && logger.level !== "silly") return;
2100
- const sqlStatement = query.replace(/\$(\d+)/g, (_, num) => {
2101
- const paramIndex = parseInt(num) - 1;
2102
- if (paramIndex >= options.length) return "INVALID_PARAM_INDEX";
2103
- return toSqlLiteral(options[paramIndex]);
2104
- });
2105
- const formattedSql = sqlFormat(sqlStatement, {
2106
- language: "postgresql",
2107
- linesBetweenQueries: 2,
2108
- indentStyle: "standard",
2109
- keywordCase: "upper",
2110
- useTabs: true,
2111
- tabWidth: 4
2112
- });
2113
- const [seconds, nanoseconds] = process.hrtime(startTime);
2114
- const durationMs = seconds * 1e3 + nanoseconds / 1e6;
2115
- let initiator = "Anonymous";
2116
- if ("userId" in queryMetadata && queryMetadata.userId)
2117
- initiator = `User Id (${queryMetadata.userId.toString()})`;
2118
- if ("isSystemUser" in queryMetadata && queryMetadata.isSystemUser) initiator = "SYSTEM";
2119
- logger.silly(`${prefix}query by ${initiator}, Query ->
2120
- ${formattedSql}`, {
2121
- durationMs
2122
- });
2123
- }
2124
- };
2125
-
2126
- // src/restura/sql/PsqlPool.ts
2127
- var { Pool } = pg;
2128
- var PsqlPool = class extends PsqlConnection {
2129
- constructor(poolConfig) {
2130
- super();
2131
- this.poolConfig = poolConfig;
2132
- this.pool = new Pool(poolConfig);
2133
- this.queryOne("SELECT NOW();", [], {
2134
- isSystemUser: true,
2135
- role: "",
2136
- host: "localhost",
2137
- ipAddress: "",
2138
- scopes: []
2139
- }).then(() => {
2140
- logger.info("Connected to PostgreSQL database");
2141
- }).catch((error) => {
2142
- logger.error("Error connecting to database", error);
2143
- process.exit(1);
2144
- });
2145
- }
2146
- pool;
2147
- async query(query, values) {
2148
- return this.pool.query(query, values);
2149
- }
2150
- };
2151
-
2152
1908
  // src/restura/sql/SqlEngine.ts
2153
1909
  import { ObjectUtils as ObjectUtils2 } from "@redskytech/core-utils";
2154
1910
  var SqlEngine = class {
@@ -2621,278 +2377,654 @@ var filterPsqlParser = peg.generate(fullGrammar, {
2621
2377
  });
2622
2378
  var filterPsqlParser_default = filterPsqlParser;
2623
2379
 
2624
- // src/restura/sql/PsqlEngine.ts
2625
- var { Client, types } = pg2;
2626
- var systemUser = {
2627
- role: "",
2628
- scopes: [],
2629
- host: "",
2630
- ipAddress: "",
2631
- isSystemUser: true
2632
- };
2633
- var PsqlEngine = class extends SqlEngine {
2634
- // 5 seconds
2635
- constructor(psqlConnectionPool, shouldListenForDbTriggers = false, scratchDatabaseSuffix = "") {
2636
- super();
2637
- this.psqlConnectionPool = psqlConnectionPool;
2638
- this.setupPgReturnTypes();
2639
- if (shouldListenForDbTriggers) {
2640
- this.setupTriggerListeners = this.listenForDbTriggers();
2641
- }
2642
- this.scratchDbName = `${psqlConnectionPool.poolConfig.database}_scratch${scratchDatabaseSuffix ? `_${scratchDatabaseSuffix}` : ""}`;
2643
- }
2644
- setupTriggerListeners;
2645
- triggerClient;
2646
- scratchDbName = "";
2647
- reconnectAttempts = 0;
2648
- MAX_RECONNECT_ATTEMPTS = 5;
2649
- INITIAL_RECONNECT_DELAY = 5e3;
2650
- async close() {
2651
- if (this.triggerClient) {
2652
- await this.triggerClient.end();
2653
- }
2654
- }
2655
- /**
2656
- * Setup the return types for the PostgreSQL connection.
2657
- * For example return DATE as a string instead of a Date object and BIGINT as a number instead of a string.
2658
- */
2659
- setupPgReturnTypes() {
2660
- const PG_TYPE_OID = {
2661
- BIGINT: 20,
2662
- DATE: 1082,
2663
- TIME: 1083,
2664
- TIMESTAMP: 1114,
2665
- TIMESTAMPTZ: 1184,
2666
- TIMETZ: 1266
2667
- };
2668
- types.setTypeParser(PG_TYPE_OID.BIGINT, (val) => val === null ? null : Number(val));
2669
- types.setTypeParser(PG_TYPE_OID.DATE, (val) => val);
2670
- types.setTypeParser(PG_TYPE_OID.TIME, (val) => val);
2671
- types.setTypeParser(PG_TYPE_OID.TIMETZ, (val) => val);
2672
- types.setTypeParser(PG_TYPE_OID.TIMESTAMP, (val) => val === null ? null : new Date(val).toISOString());
2673
- types.setTypeParser(PG_TYPE_OID.TIMESTAMPTZ, (val) => val === null ? null : new Date(val).toISOString());
2380
+ // src/restura/sql/psqlSchemaUtils.ts
2381
+ import getDiff from "@wmfs/pg-diff-sync";
2382
+ import pgInfo from "@wmfs/pg-info";
2383
+ import pg2 from "pg";
2384
+
2385
+ // src/restura/sql/PsqlPool.ts
2386
+ import pg from "pg";
2387
+
2388
+ // src/restura/sql/PsqlConnection.ts
2389
+ import crypto from "crypto";
2390
+ import { format as sqlFormat } from "sql-formatter";
2391
+ import { z as z4 } from "zod";
2392
+ var PsqlConnection = class {
2393
+ instanceId;
2394
+ constructor(instanceId) {
2395
+ this.instanceId = instanceId || crypto.randomUUID();
2674
2396
  }
2675
- async reconnectTriggerClient() {
2676
- if (this.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) {
2677
- logger.error("Max reconnection attempts reached for trigger client. Stopping reconnection attempts.");
2678
- return;
2679
- }
2680
- if (this.triggerClient) {
2681
- try {
2682
- await this.triggerClient.end();
2683
- } catch (error) {
2684
- logger.error(`Error closing trigger client: ${error}`);
2397
+ async queryOne(query, options, requesterDetails) {
2398
+ const formattedQuery = questionMarksToOrderedParams(query);
2399
+ const meta = { connectionInstanceId: this.instanceId, ...requesterDetails };
2400
+ const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
2401
+ `;
2402
+ const startTime = process.hrtime();
2403
+ try {
2404
+ const response = await this.query(queryMetadata + formattedQuery, options);
2405
+ if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
2406
+ else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
2407
+ this.logSqlStatement(formattedQuery, options, meta, startTime);
2408
+ return response.rows[0];
2409
+ } catch (error) {
2410
+ this.logSqlStatement(formattedQuery, options, meta, startTime);
2411
+ if (RsError.isRsError(error)) throw error;
2412
+ if (error?.routine === "_bt_check_unique") {
2413
+ throw new RsError("DUPLICATE", error.message);
2685
2414
  }
2415
+ throw new RsError("DATABASE_ERROR", `${error.message}`);
2686
2416
  }
2687
- const delay = this.INITIAL_RECONNECT_DELAY * Math.pow(2, this.reconnectAttempts);
2688
- logger.info(
2689
- `Attempting to reconnect trigger client in ${delay / 1e3} seconds... (Attempt ${this.reconnectAttempts + 1}/${this.MAX_RECONNECT_ATTEMPTS})`
2690
- );
2691
- await new Promise((resolve2) => setTimeout(resolve2, delay));
2692
- this.reconnectAttempts++;
2417
+ }
2418
+ async queryOneSchema(query, params, requesterDetails, zodSchema) {
2419
+ const result = await this.queryOne(query, params, requesterDetails);
2693
2420
  try {
2694
- await this.listenForDbTriggers();
2695
- this.reconnectAttempts = 0;
2421
+ return zodSchema.parse(result);
2696
2422
  } catch (error) {
2697
- logger.error(`Reconnection attempt ${this.reconnectAttempts} failed: ${error}`);
2698
- if (this.reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS) {
2699
- await this.reconnectTriggerClient();
2423
+ if (error instanceof z4.ZodError) {
2424
+ logger.error("Invalid data returned from database:");
2425
+ logger.trace("\n" + JSON.stringify(result, null, 2));
2426
+ logger.error("\n" + z4.prettifyError(error));
2427
+ } else {
2428
+ logger.error(error);
2700
2429
  }
2430
+ throw new RsError("DATABASE_ERROR", `Invalid data returned from database`);
2701
2431
  }
2702
2432
  }
2703
- async listenForDbTriggers() {
2704
- this.triggerClient = new Client({
2705
- user: this.psqlConnectionPool.poolConfig.user,
2706
- host: this.psqlConnectionPool.poolConfig.host,
2707
- database: this.psqlConnectionPool.poolConfig.database,
2708
- password: this.psqlConnectionPool.poolConfig.password,
2709
- port: this.psqlConnectionPool.poolConfig.port,
2710
- connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
2711
- });
2433
+ async runQuery(query, options, requesterDetails) {
2434
+ const formattedQuery = questionMarksToOrderedParams(query);
2435
+ const meta = { connectionInstanceId: this.instanceId, ...requesterDetails };
2436
+ const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
2437
+ `;
2438
+ const startTime = process.hrtime();
2712
2439
  try {
2713
- await this.triggerClient.connect();
2714
- const promises = [];
2715
- promises.push(this.triggerClient.query("LISTEN insert"));
2716
- promises.push(this.triggerClient.query("LISTEN update"));
2717
- promises.push(this.triggerClient.query("LISTEN delete"));
2718
- await Promise.all(promises);
2719
- this.triggerClient.on("error", async (error) => {
2720
- logger.error(`Trigger client error: ${error}`);
2721
- await this.reconnectTriggerClient();
2722
- });
2723
- this.triggerClient.on("notification", async (msg) => {
2724
- if (msg.channel === "insert" || msg.channel === "update" || msg.channel === "delete") {
2725
- const payload = ObjectUtils3.safeParse(msg.payload);
2726
- await this.handleTrigger(payload, msg.channel.toUpperCase());
2727
- }
2728
- });
2729
- logger.info("Successfully connected to database triggers");
2440
+ const response = await this.query(queryMetadata + formattedQuery, options);
2441
+ this.logSqlStatement(formattedQuery, options, meta, startTime);
2442
+ return response.rows;
2730
2443
  } catch (error) {
2731
- logger.error(`Failed to setup trigger listeners: ${error}`);
2732
- await this.reconnectTriggerClient();
2444
+ this.logSqlStatement(formattedQuery, options, meta, startTime);
2445
+ if (error?.routine === "_bt_check_unique") {
2446
+ throw new RsError("DUPLICATE", error.message);
2447
+ }
2448
+ throw new RsError("DATABASE_ERROR", `${error.message}`);
2733
2449
  }
2734
2450
  }
2735
- async handleTrigger(payload, mutationType) {
2736
- if (payload.queryMetadata && payload.queryMetadata.connectionInstanceId === this.psqlConnectionPool.instanceId) {
2737
- await eventManager_default.fireActionFromDbTrigger({ queryMetadata: payload.queryMetadata, mutationType }, payload);
2451
+ async runQuerySchema(query, params, requesterDetails, zodSchema) {
2452
+ const result = await this.runQuery(query, params, requesterDetails);
2453
+ try {
2454
+ return z4.array(zodSchema).parse(result);
2455
+ } catch (error) {
2456
+ if (error instanceof z4.ZodError) {
2457
+ logger.error("Invalid data returned from database:");
2458
+ logger.trace("\n" + JSON.stringify(result, null, 2));
2459
+ logger.error("\n" + z4.prettifyError(error));
2460
+ } else {
2461
+ logger.error(error);
2462
+ }
2463
+ throw new RsError("DATABASE_ERROR", `Invalid data returned from database`);
2738
2464
  }
2739
2465
  }
2740
- async createDatabaseFromSchema(schema, connection) {
2741
- const sqlFullStatement = this.generateDatabaseSchemaFromSchema(schema);
2742
- await connection.runQuery(sqlFullStatement, [], systemUser);
2743
- return sqlFullStatement;
2466
+ logSqlStatement(query, options, queryMetadata, startTime, prefix = "") {
2467
+ if (logger.level !== "trace") return;
2468
+ const sqlStatement = query.replace(/\$(\d+)/g, (_, num) => {
2469
+ const paramIndex = parseInt(num) - 1;
2470
+ if (paramIndex >= options.length) return "INVALID_PARAM_INDEX";
2471
+ return toSqlLiteral(options[paramIndex]);
2472
+ });
2473
+ const formattedSql = sqlFormat(sqlStatement, {
2474
+ language: "postgresql",
2475
+ linesBetweenQueries: 2,
2476
+ indentStyle: "standard",
2477
+ keywordCase: "upper",
2478
+ useTabs: true,
2479
+ tabWidth: 4
2480
+ });
2481
+ const [seconds, nanoseconds] = process.hrtime(startTime);
2482
+ const durationMs = seconds * 1e3 + nanoseconds / 1e6;
2483
+ let initiator = "Anonymous";
2484
+ if ("userId" in queryMetadata && queryMetadata.userId)
2485
+ initiator = `User Id (${queryMetadata.userId.toString()})`;
2486
+ if ("isSystemUser" in queryMetadata && queryMetadata.isSystemUser) initiator = "SYSTEM";
2487
+ logger.trace(`${prefix}query by ${initiator}, Query ->
2488
+ ${formattedSql}`, {
2489
+ durationMs
2490
+ });
2744
2491
  }
2745
- generateDatabaseSchemaFromSchema(schema) {
2746
- const sqlStatements = [];
2747
- const indexes = [];
2748
- const triggers = [];
2749
- for (const table of schema.database) {
2750
- if (table.notify) {
2751
- triggers.push(this.createInsertTriggers(table.name, table.notify));
2752
- triggers.push(this.createUpdateTrigger(table.name, table.notify));
2753
- triggers.push(this.createDeleteTrigger(table.name, table.notify));
2754
- }
2755
- let sql = `CREATE TABLE "${table.name}"
2756
- ( `;
2757
- const tableColumns = [];
2758
- for (const column of table.columns) {
2759
- let columnSql = "";
2760
- columnSql += ` "${column.name}" ${this.schemaToPsqlType(column)}`;
2761
- let value = column.value;
2762
- if (column.type === "JSON") value = "";
2763
- if (column.type === "JSONB") value = "";
2764
- if (column.type === "DECIMAL" && value) {
2765
- value = value.replace("-", ",").replace(/['"]/g, "");
2766
- }
2767
- if (value && column.type !== "ENUM") {
2768
- columnSql += `(${value})`;
2769
- } else if (column.length) columnSql += `(${column.length})`;
2770
- if (column.isPrimary) {
2771
- columnSql += " PRIMARY KEY ";
2772
- }
2773
- if (column.isUnique) {
2774
- columnSql += ` CONSTRAINT "${table.name}_${column.name}_unique_index" UNIQUE `;
2775
- }
2776
- if (column.isNullable) columnSql += " NULL";
2777
- else columnSql += " NOT NULL";
2778
- if (column.default) columnSql += ` DEFAULT ${column.default}`;
2779
- if (value && column.type === "ENUM") {
2780
- columnSql += ` CHECK ("${column.name}" IN (${value}))`;
2781
- }
2782
- tableColumns.push(columnSql);
2783
- }
2784
- sql += tableColumns.join(", \n");
2785
- for (const index of table.indexes) {
2786
- if (!index.isPrimaryKey) {
2787
- let unique = " ";
2788
- if (index.isUnique) unique = "UNIQUE ";
2789
- let indexSQL = ` CREATE ${unique}INDEX "${index.name}" ON "${table.name}"`;
2790
- indexSQL += ` (${index.columns.map((item) => `"${item}" ${index.order}`).join(", ")})`;
2791
- indexSQL += index.where ? ` WHERE ${index.where}` : "";
2792
- indexSQL += ";";
2793
- indexes.push(indexSQL);
2794
- }
2492
+ };
2493
+
2494
+ // src/restura/sql/PsqlPool.ts
2495
+ var { Pool } = pg;
2496
+ var PsqlPool = class extends PsqlConnection {
2497
+ constructor(poolConfig) {
2498
+ super();
2499
+ this.poolConfig = poolConfig;
2500
+ if (poolConfig.connectionString) {
2501
+ let url;
2502
+ try {
2503
+ url = new URL(poolConfig.connectionString);
2504
+ } catch {
2505
+ throw new Error(`Invalid connectionString: ${poolConfig.connectionString}`);
2795
2506
  }
2796
- sql += "\n);";
2797
- sqlStatements.push(sql);
2507
+ poolConfig.host = url.hostname;
2508
+ poolConfig.port = url.port ? parseInt(url.port) : 5432;
2509
+ poolConfig.user = url.username;
2510
+ poolConfig.password = url.password;
2511
+ poolConfig.database = url.pathname.replace(/^\//, "");
2798
2512
  }
2799
- for (const table of schema.database) {
2800
- if (!table.foreignKeys.length) continue;
2801
- const sql = `ALTER TABLE "${table.name}" `;
2802
- const constraints = [];
2803
- for (const foreignKey of table.foreignKeys) {
2804
- let constraint = ` ADD CONSTRAINT "${foreignKey.name}"
2513
+ this.pool = new Pool(poolConfig);
2514
+ this.queryOne("SELECT NOW();", [], {
2515
+ isSystemUser: true,
2516
+ role: "",
2517
+ host: "localhost",
2518
+ ipAddress: "",
2519
+ scopes: []
2520
+ }).then(() => {
2521
+ logger.info("Connected to PostgreSQL database");
2522
+ }).catch((error) => {
2523
+ logger.error("Error connecting to database", error);
2524
+ process.exit(1);
2525
+ });
2526
+ }
2527
+ pool;
2528
+ async query(query, values) {
2529
+ return this.pool.query(query, values);
2530
+ }
2531
+ };
2532
+
2533
+ // src/restura/sql/psqlSchemaUtils.ts
2534
+ var { Client } = pg2;
2535
+ var systemUser = {
2536
+ role: "",
2537
+ scopes: [],
2538
+ host: "",
2539
+ ipAddress: "",
2540
+ isSystemUser: true
2541
+ };
2542
+ function schemaToPsqlType(column) {
2543
+ if (column.hasAutoIncrement) return "BIGSERIAL";
2544
+ if (column.type === "ENUM") return "TEXT";
2545
+ if (column.type === "DATETIME") return "TIMESTAMPTZ";
2546
+ if (column.type === "MEDIUMINT") return "INT";
2547
+ return column.type;
2548
+ }
2549
+ function createInsertTriggerSql(tableName, notify) {
2550
+ if (!notify) return "";
2551
+ if (notify === "ALL") {
2552
+ return `
2553
+ CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
2554
+ RETURNS TRIGGER AS $$
2555
+ DECLARE
2556
+ query_metadata JSON;
2557
+ BEGIN
2558
+ SELECT INTO query_metadata
2559
+ (regexp_match(
2560
+ current_query(),
2561
+ '^--QUERY_METADATA\\(({.*})', 'n'
2562
+ ))[1]::json;
2563
+
2564
+ PERFORM pg_notify(
2565
+ 'insert',
2566
+ json_build_object(
2567
+ 'table', '${tableName}',
2568
+ 'queryMetadata', query_metadata,
2569
+ 'insertedId', NEW.id,
2570
+ 'record', NEW
2571
+ )::text
2572
+ );
2573
+
2574
+ RETURN NEW;
2575
+ END;
2576
+ $$ LANGUAGE plpgsql;
2577
+
2578
+ CREATE OR REPLACE TRIGGER "${tableName}_insert"
2579
+ AFTER INSERT ON "${tableName}"
2580
+ FOR EACH ROW
2581
+ EXECUTE FUNCTION notify_${tableName}_insert();
2582
+ `;
2583
+ }
2584
+ const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
2585
+ return `
2586
+ CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
2587
+ RETURNS TRIGGER AS $$
2588
+ DECLARE
2589
+ query_metadata JSON;
2590
+ BEGIN
2591
+ SELECT INTO query_metadata
2592
+ (regexp_match(
2593
+ current_query(),
2594
+ '^--QUERY_METADATA\\(({.*})', 'n'
2595
+ ))[1]::json;
2596
+
2597
+ PERFORM pg_notify(
2598
+ 'insert',
2599
+ json_build_object(
2600
+ 'table', '${tableName}',
2601
+ 'queryMetadata', query_metadata,
2602
+ 'insertedId', NEW.id,
2603
+ 'record', json_build_object(
2604
+ ${notifyColumnNewBuildString}
2605
+ )
2606
+ )::text
2607
+ );
2608
+
2609
+ RETURN NEW;
2610
+ END;
2611
+ $$ LANGUAGE plpgsql;
2612
+
2613
+ CREATE OR REPLACE TRIGGER "${tableName}_insert"
2614
+ AFTER INSERT ON "${tableName}"
2615
+ FOR EACH ROW
2616
+ EXECUTE FUNCTION notify_${tableName}_insert();
2617
+ `;
2618
+ }
2619
+ function createUpdateTriggerSql(tableName, notify) {
2620
+ if (!notify) return "";
2621
+ if (notify === "ALL") {
2622
+ return `
2623
+ CREATE OR REPLACE FUNCTION notify_${tableName}_update()
2624
+ RETURNS TRIGGER AS $$
2625
+ DECLARE
2626
+ query_metadata JSON;
2627
+ BEGIN
2628
+ SELECT INTO query_metadata
2629
+ (regexp_match(
2630
+ current_query(),
2631
+ '^--QUERY_METADATA\\(({.*})', 'n'
2632
+ ))[1]::json;
2633
+
2634
+ PERFORM pg_notify(
2635
+ 'update',
2636
+ json_build_object(
2637
+ 'table', '${tableName}',
2638
+ 'queryMetadata', query_metadata,
2639
+ 'changedId', NEW.id,
2640
+ 'record', NEW,
2641
+ 'previousRecord', OLD
2642
+ )::text
2643
+ );
2644
+ RETURN NEW;
2645
+ END;
2646
+ $$ LANGUAGE plpgsql;
2647
+
2648
+ CREATE OR REPLACE TRIGGER ${tableName}_update
2649
+ AFTER UPDATE ON "${tableName}"
2650
+ FOR EACH ROW
2651
+ EXECUTE FUNCTION notify_${tableName}_update();
2652
+ `;
2653
+ }
2654
+ const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
2655
+ const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
2656
+ return `
2657
+ CREATE OR REPLACE FUNCTION notify_${tableName}_update()
2658
+ RETURNS TRIGGER AS $$
2659
+ DECLARE
2660
+ query_metadata JSON;
2661
+ BEGIN
2662
+ SELECT INTO query_metadata
2663
+ (regexp_match(
2664
+ current_query(),
2665
+ '^--QUERY_METADATA\\(({.*})', 'n'
2666
+ ))[1]::json;
2667
+
2668
+ PERFORM pg_notify(
2669
+ 'update',
2670
+ json_build_object(
2671
+ 'table', '${tableName}',
2672
+ 'queryMetadata', query_metadata,
2673
+ 'changedId', NEW.id,
2674
+ 'record', json_build_object(
2675
+ ${notifyColumnNewBuildString}
2676
+ ),
2677
+ 'previousRecord', json_build_object(
2678
+ ${notifyColumnOldBuildString}
2679
+ )
2680
+ )::text
2681
+ );
2682
+ RETURN NEW;
2683
+ END;
2684
+ $$ LANGUAGE plpgsql;
2685
+
2686
+ CREATE OR REPLACE TRIGGER ${tableName}_update
2687
+ AFTER UPDATE ON "${tableName}"
2688
+ FOR EACH ROW
2689
+ EXECUTE FUNCTION notify_${tableName}_update();
2690
+ `;
2691
+ }
2692
+ function createDeleteTriggerSql(tableName, notify) {
2693
+ if (!notify) return "";
2694
+ if (notify === "ALL") {
2695
+ return `
2696
+ CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
2697
+ RETURNS TRIGGER AS $$
2698
+ DECLARE
2699
+ query_metadata JSON;
2700
+ BEGIN
2701
+ SELECT INTO query_metadata
2702
+ (regexp_match(
2703
+ current_query(),
2704
+ '^--QUERY_METADATA\\(({.*})', 'n'
2705
+ ))[1]::json;
2706
+
2707
+ PERFORM pg_notify(
2708
+ 'delete',
2709
+ json_build_object(
2710
+ 'table', '${tableName}',
2711
+ 'queryMetadata', query_metadata,
2712
+ 'deletedId', OLD.id,
2713
+ 'previousRecord', OLD
2714
+ )::text
2715
+ );
2716
+ RETURN OLD;
2717
+ END;
2718
+ $$ LANGUAGE plpgsql;
2719
+
2720
+ CREATE OR REPLACE TRIGGER "${tableName}_delete"
2721
+ AFTER DELETE ON "${tableName}"
2722
+ FOR EACH ROW
2723
+ EXECUTE FUNCTION notify_${tableName}_delete();
2724
+ `;
2725
+ }
2726
+ const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
2727
+ return `
2728
+ CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
2729
+ RETURNS TRIGGER AS $$
2730
+ DECLARE
2731
+ query_metadata JSON;
2732
+ BEGIN
2733
+ SELECT INTO query_metadata
2734
+ (regexp_match(
2735
+ current_query(),
2736
+ '^--QUERY_METADATA\\(({.*})', 'n'
2737
+ ))[1]::json;
2738
+
2739
+ PERFORM pg_notify(
2740
+ 'delete',
2741
+ json_build_object(
2742
+ 'table', '${tableName}',
2743
+ 'queryMetadata', query_metadata,
2744
+ 'deletedId', OLD.id,
2745
+ 'previousRecord', json_build_object(
2746
+ ${notifyColumnOldBuildString}
2747
+ )
2748
+ )::text
2749
+ );
2750
+ RETURN OLD;
2751
+ END;
2752
+ $$ LANGUAGE plpgsql;
2753
+
2754
+ CREATE OR REPLACE TRIGGER "${tableName}_delete"
2755
+ AFTER DELETE ON "${tableName}"
2756
+ FOR EACH ROW
2757
+ EXECUTE FUNCTION notify_${tableName}_delete();
2758
+ `;
2759
+ }
2760
+ function generateDatabaseSchemaFromSchema(schema) {
2761
+ const sqlStatements = [];
2762
+ const indexes = [];
2763
+ const triggers = [];
2764
+ for (const table of schema.database) {
2765
+ if (table.notify) {
2766
+ triggers.push(createInsertTriggerSql(table.name, table.notify));
2767
+ triggers.push(createUpdateTriggerSql(table.name, table.notify));
2768
+ triggers.push(createDeleteTriggerSql(table.name, table.notify));
2769
+ }
2770
+ let sql = `CREATE TABLE "${table.name}"
2771
+ ( `;
2772
+ const tableColumns = [];
2773
+ for (const column of table.columns) {
2774
+ let columnSql = "";
2775
+ columnSql += ` "${column.name}" ${schemaToPsqlType(column)}`;
2776
+ let value = column.value;
2777
+ if (column.type === "JSON") value = "";
2778
+ if (column.type === "JSONB") value = "";
2779
+ if (column.type === "DECIMAL" && value) {
2780
+ value = value.replace("-", ",").replace(/['"]/g, "");
2781
+ }
2782
+ if (value && column.type !== "ENUM") {
2783
+ columnSql += `(${value})`;
2784
+ } else if (column.length) columnSql += `(${column.length})`;
2785
+ if (column.isPrimary) {
2786
+ columnSql += " PRIMARY KEY ";
2787
+ }
2788
+ if (column.isUnique) {
2789
+ columnSql += ` CONSTRAINT "${table.name}_${column.name}_unique_index" UNIQUE `;
2790
+ }
2791
+ if (column.isNullable) columnSql += " NULL";
2792
+ else columnSql += " NOT NULL";
2793
+ if (column.default) columnSql += ` DEFAULT ${column.default}`;
2794
+ if (value && column.type === "ENUM") {
2795
+ columnSql += ` CHECK ("${column.name}" IN (${value}))`;
2796
+ }
2797
+ tableColumns.push(columnSql);
2798
+ }
2799
+ sql += tableColumns.join(", \n");
2800
+ for (const index of table.indexes) {
2801
+ if (!index.isPrimaryKey) {
2802
+ let unique = " ";
2803
+ if (index.isUnique) unique = "UNIQUE ";
2804
+ let indexSQL = ` CREATE ${unique}INDEX "${index.name}" ON "${table.name}"`;
2805
+ indexSQL += ` (${index.columns.map((item) => `"${item}" ${index.order}`).join(", ")})`;
2806
+ indexSQL += index.where ? ` WHERE ${index.where}` : "";
2807
+ indexSQL += ";";
2808
+ indexes.push(indexSQL);
2809
+ }
2810
+ }
2811
+ sql += "\n);";
2812
+ sqlStatements.push(sql);
2813
+ }
2814
+ for (const table of schema.database) {
2815
+ if (!table.foreignKeys.length) continue;
2816
+ const sql = `ALTER TABLE "${table.name}" `;
2817
+ const constraints = [];
2818
+ for (const foreignKey of table.foreignKeys) {
2819
+ let constraint = ` ADD CONSTRAINT "${foreignKey.name}"
2805
2820
  FOREIGN KEY ("${foreignKey.column}") REFERENCES "${foreignKey.refTable}" ("${foreignKey.refColumn}")`;
2806
- constraint += ` ON DELETE ${foreignKey.onDelete}`;
2807
- constraint += ` ON UPDATE ${foreignKey.onUpdate}`;
2808
- constraints.push(constraint);
2809
- }
2810
- sqlStatements.push(sql + constraints.join(",\n") + ";");
2811
- }
2812
- for (const table of schema.database) {
2813
- if (!table.checkConstraints.length) continue;
2814
- const sql = `ALTER TABLE "${table.name}" `;
2815
- const constraints = [];
2816
- for (const check of table.checkConstraints) {
2817
- const constraint = `ADD CONSTRAINT "${check.name}" CHECK (${check.check})`;
2818
- constraints.push(constraint);
2819
- }
2820
- sqlStatements.push(sql + constraints.join(",\n") + ";");
2821
- }
2822
- sqlStatements.push(indexes.join("\n"));
2823
- sqlStatements.push(triggers.join("\n"));
2824
- return sqlStatements.join("\n\n");
2825
- }
2826
- async getNewPublicSchemaAndScratchPool() {
2827
- const scratchDbExists = await this.psqlConnectionPool.runQuery(
2828
- `SELECT *
2829
- FROM pg_database
2830
- WHERE datname = '${this.scratchDbName}';`,
2831
- [],
2832
- systemUser
2821
+ constraint += ` ON DELETE ${foreignKey.onDelete}`;
2822
+ constraint += ` ON UPDATE ${foreignKey.onUpdate}`;
2823
+ constraints.push(constraint);
2824
+ }
2825
+ sqlStatements.push(sql + constraints.join(",\n") + ";");
2826
+ }
2827
+ for (const table of schema.database) {
2828
+ if (!table.checkConstraints.length) continue;
2829
+ const sql = `ALTER TABLE "${table.name}" `;
2830
+ const constraints = [];
2831
+ for (const check of table.checkConstraints) {
2832
+ const constraint = `ADD CONSTRAINT "${check.name}" CHECK (${check.check})`;
2833
+ constraints.push(constraint);
2834
+ }
2835
+ sqlStatements.push(sql + constraints.join(",\n") + ";");
2836
+ }
2837
+ sqlStatements.push(indexes.join("\n"));
2838
+ sqlStatements.push(triggers.join("\n"));
2839
+ return sqlStatements.join("\n\n");
2840
+ }
2841
+ async function getNewPublicSchemaAndScratchPool(targetPool, scratchDbName) {
2842
+ const scratchDbExists = await targetPool.runQuery(
2843
+ `SELECT * FROM pg_database WHERE datname = ?;`,
2844
+ [scratchDbName],
2845
+ systemUser
2846
+ );
2847
+ if (scratchDbExists.length === 0) {
2848
+ await targetPool.runQuery(`CREATE DATABASE ${escapeColumnName(scratchDbName)};`, [], systemUser);
2849
+ }
2850
+ const scratchPool = new PsqlPool({
2851
+ host: targetPool.poolConfig.host,
2852
+ port: targetPool.poolConfig.port,
2853
+ user: targetPool.poolConfig.user,
2854
+ database: scratchDbName,
2855
+ password: targetPool.poolConfig.password,
2856
+ max: targetPool.poolConfig.max,
2857
+ idleTimeoutMillis: targetPool.poolConfig.idleTimeoutMillis,
2858
+ connectionTimeoutMillis: targetPool.poolConfig.connectionTimeoutMillis
2859
+ });
2860
+ await scratchPool.runQuery(`DROP SCHEMA public CASCADE;`, [], systemUser);
2861
+ await scratchPool.runQuery(`CREATE SCHEMA public AUTHORIZATION ${escapeColumnName(targetPool.poolConfig.user)};`, [], systemUser);
2862
+ const schemaComment = await targetPool.runQuery(
2863
+ `
2864
+ SELECT pg_description.description
2865
+ FROM pg_description
2866
+ JOIN pg_namespace ON pg_namespace.oid = pg_description.objoid
2867
+ WHERE pg_namespace.nspname = 'public';`,
2868
+ [],
2869
+ systemUser
2870
+ );
2871
+ if (schemaComment[0]?.description) {
2872
+ await scratchPool.runQuery(`COMMENT ON SCHEMA public IS $1;`, [schemaComment[0].description], systemUser);
2873
+ }
2874
+ return scratchPool;
2875
+ }
2876
+ async function diffDatabaseToSchema(schema, targetPool, scratchDbName) {
2877
+ let scratchPool;
2878
+ let originalClient;
2879
+ let scratchClient;
2880
+ try {
2881
+ scratchPool = await getNewPublicSchemaAndScratchPool(targetPool, scratchDbName);
2882
+ const sqlFullStatement = generateDatabaseSchemaFromSchema(schema);
2883
+ await scratchPool.runQuery(sqlFullStatement, [], systemUser);
2884
+ const connectionConfig = {
2885
+ host: targetPool.poolConfig.host,
2886
+ port: targetPool.poolConfig.port,
2887
+ user: targetPool.poolConfig.user,
2888
+ password: targetPool.poolConfig.password,
2889
+ ssl: targetPool.poolConfig.ssl
2890
+ };
2891
+ originalClient = new Client({ ...connectionConfig, database: targetPool.poolConfig.database });
2892
+ scratchClient = new Client({ ...connectionConfig, database: scratchDbName });
2893
+ await Promise.all([originalClient.connect(), scratchClient.connect()]);
2894
+ const [info1, info2] = await Promise.all([
2895
+ pgInfo({ client: originalClient }),
2896
+ pgInfo({ client: scratchClient })
2897
+ ]);
2898
+ const diff = getDiff(info1, info2);
2899
+ return diff.join("\n");
2900
+ } finally {
2901
+ const cleanups = [];
2902
+ if (originalClient) cleanups.push(originalClient.end());
2903
+ if (scratchClient) cleanups.push(scratchClient.end());
2904
+ if (scratchPool) cleanups.push(scratchPool.pool.end());
2905
+ await Promise.allSettled(cleanups);
2906
+ }
2907
+ }
2908
+
2909
+ // src/restura/sql/PsqlEngine.ts
2910
+ var { Client: Client2, types } = pg3;
2911
+ var PsqlEngine = class extends SqlEngine {
2912
+ // 5 seconds
2913
+ constructor(psqlConnectionPool, shouldListenForDbTriggers = false, scratchDatabaseSuffix = "") {
2914
+ super();
2915
+ this.psqlConnectionPool = psqlConnectionPool;
2916
+ this.setupPgReturnTypes();
2917
+ if (shouldListenForDbTriggers) {
2918
+ this.setupTriggerListeners = this.listenForDbTriggers();
2919
+ }
2920
+ this.scratchDbName = `${psqlConnectionPool.poolConfig.database}_scratch${scratchDatabaseSuffix ? `_${scratchDatabaseSuffix}` : ""}`;
2921
+ }
2922
+ setupTriggerListeners;
2923
+ triggerClient;
2924
+ scratchDbName = "";
2925
+ reconnectAttempts = 0;
2926
+ MAX_RECONNECT_ATTEMPTS = 5;
2927
+ INITIAL_RECONNECT_DELAY = 5e3;
2928
+ async close() {
2929
+ if (this.triggerClient) {
2930
+ await this.triggerClient.end();
2931
+ }
2932
+ }
2933
+ /**
2934
+ * Setup the return types for the PostgreSQL connection.
2935
+ * For example return DATE as a string instead of a Date object and BIGINT as a number instead of a string.
2936
+ */
2937
+ setupPgReturnTypes() {
2938
+ const PG_TYPE_OID = {
2939
+ BIGINT: 20,
2940
+ DATE: 1082,
2941
+ TIME: 1083,
2942
+ TIMESTAMP: 1114,
2943
+ TIMESTAMPTZ: 1184,
2944
+ TIMETZ: 1266
2945
+ };
2946
+ types.setTypeParser(PG_TYPE_OID.BIGINT, (val) => val === null ? null : Number(val));
2947
+ types.setTypeParser(PG_TYPE_OID.DATE, (val) => val);
2948
+ types.setTypeParser(PG_TYPE_OID.TIME, (val) => val);
2949
+ types.setTypeParser(PG_TYPE_OID.TIMETZ, (val) => val);
2950
+ types.setTypeParser(PG_TYPE_OID.TIMESTAMP, (val) => val === null ? null : new Date(val).toISOString());
2951
+ types.setTypeParser(PG_TYPE_OID.TIMESTAMPTZ, (val) => val === null ? null : new Date(val).toISOString());
2952
+ }
2953
+ async reconnectTriggerClient() {
2954
+ if (this.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) {
2955
+ logger.error("Max reconnection attempts reached for trigger client. Stopping reconnection attempts.");
2956
+ return;
2957
+ }
2958
+ if (this.triggerClient) {
2959
+ try {
2960
+ await this.triggerClient.end();
2961
+ } catch (error) {
2962
+ logger.error(`Error closing trigger client: ${error}`);
2963
+ }
2964
+ }
2965
+ const delay = this.INITIAL_RECONNECT_DELAY * Math.pow(2, this.reconnectAttempts);
2966
+ logger.info(
2967
+ `Attempting to reconnect trigger client in ${delay / 1e3} seconds... (Attempt ${this.reconnectAttempts + 1}/${this.MAX_RECONNECT_ATTEMPTS})`
2833
2968
  );
2834
- if (scratchDbExists.length === 0) {
2835
- await this.psqlConnectionPool.runQuery(`CREATE DATABASE ${this.scratchDbName};`, [], systemUser);
2969
+ await new Promise((resolve2) => setTimeout(resolve2, delay));
2970
+ this.reconnectAttempts++;
2971
+ try {
2972
+ await this.listenForDbTriggers();
2973
+ this.reconnectAttempts = 0;
2974
+ } catch (error) {
2975
+ logger.error(`Reconnection attempt ${this.reconnectAttempts} failed: ${error}`);
2976
+ if (this.reconnectAttempts < this.MAX_RECONNECT_ATTEMPTS) {
2977
+ await this.reconnectTriggerClient();
2978
+ }
2836
2979
  }
2837
- const scratchPool = new PsqlPool({
2838
- host: this.psqlConnectionPool.poolConfig.host,
2839
- port: this.psqlConnectionPool.poolConfig.port,
2980
+ }
2981
+ async listenForDbTriggers() {
2982
+ this.triggerClient = new Client2({
2840
2983
  user: this.psqlConnectionPool.poolConfig.user,
2841
- database: this.scratchDbName,
2984
+ host: this.psqlConnectionPool.poolConfig.host,
2985
+ database: this.psqlConnectionPool.poolConfig.database,
2842
2986
  password: this.psqlConnectionPool.poolConfig.password,
2843
- max: this.psqlConnectionPool.poolConfig.max,
2844
- idleTimeoutMillis: this.psqlConnectionPool.poolConfig.idleTimeoutMillis,
2987
+ port: this.psqlConnectionPool.poolConfig.port,
2845
2988
  connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
2846
2989
  });
2847
- await scratchPool.runQuery(`DROP SCHEMA public CASCADE;`, [], systemUser);
2848
- await scratchPool.runQuery(
2849
- `CREATE SCHEMA public AUTHORIZATION ${this.psqlConnectionPool.poolConfig.user};`,
2850
- [],
2851
- systemUser
2852
- );
2853
- const schemaComment = await this.psqlConnectionPool.runQuery(
2854
- `
2855
- SELECT pg_description.description
2856
- FROM pg_description
2857
- JOIN pg_namespace ON pg_namespace.oid = pg_description.objoid
2858
- WHERE pg_namespace.nspname = 'public';`,
2859
- [],
2860
- systemUser
2861
- );
2862
- if (schemaComment[0]?.description) {
2863
- await scratchPool.runQuery(
2864
- `COMMENT ON SCHEMA public IS '${schemaComment[0]?.description}';`,
2865
- [],
2866
- systemUser
2867
- );
2990
+ try {
2991
+ await this.triggerClient.connect();
2992
+ const promises = [];
2993
+ promises.push(this.triggerClient.query("LISTEN insert"));
2994
+ promises.push(this.triggerClient.query("LISTEN update"));
2995
+ promises.push(this.triggerClient.query("LISTEN delete"));
2996
+ await Promise.all(promises);
2997
+ this.triggerClient.on("error", async (error) => {
2998
+ logger.error(`Trigger client error: ${error}`);
2999
+ await this.reconnectTriggerClient();
3000
+ });
3001
+ this.triggerClient.on("notification", async (msg) => {
3002
+ if (msg.channel === "insert" || msg.channel === "update" || msg.channel === "delete") {
3003
+ const payload = ObjectUtils3.safeParse(msg.payload);
3004
+ await this.handleTrigger(payload, msg.channel.toUpperCase());
3005
+ }
3006
+ });
3007
+ logger.info("Successfully connected to database triggers");
3008
+ } catch (error) {
3009
+ logger.error(`Failed to setup trigger listeners: ${error}`);
3010
+ await this.reconnectTriggerClient();
3011
+ }
3012
+ }
3013
+ async handleTrigger(payload, mutationType) {
3014
+ if (payload.queryMetadata && payload.queryMetadata.connectionInstanceId === this.psqlConnectionPool.instanceId) {
3015
+ await eventManager_default.fireActionFromDbTrigger({ queryMetadata: payload.queryMetadata, mutationType }, payload);
2868
3016
  }
2869
- return scratchPool;
3017
+ }
3018
+ async createDatabaseFromSchema(schema, connection) {
3019
+ const sqlFullStatement = this.generateDatabaseSchemaFromSchema(schema);
3020
+ await connection.runQuery(sqlFullStatement, [], systemUser);
3021
+ return sqlFullStatement;
3022
+ }
3023
+ generateDatabaseSchemaFromSchema(schema) {
3024
+ return generateDatabaseSchemaFromSchema(schema);
2870
3025
  }
2871
3026
  async diffDatabaseToSchema(schema) {
2872
- const scratchPool = await this.getNewPublicSchemaAndScratchPool();
2873
- await this.createDatabaseFromSchema(schema, scratchPool);
2874
- const originalClient = new Client({
2875
- database: this.psqlConnectionPool.poolConfig.database,
2876
- user: this.psqlConnectionPool.poolConfig.user,
2877
- password: this.psqlConnectionPool.poolConfig.password,
2878
- host: this.psqlConnectionPool.poolConfig.host,
2879
- port: this.psqlConnectionPool.poolConfig.port
2880
- });
2881
- const scratchClient = new Client({
2882
- database: this.scratchDbName,
2883
- user: this.psqlConnectionPool.poolConfig.user,
2884
- password: this.psqlConnectionPool.poolConfig.password,
2885
- host: this.psqlConnectionPool.poolConfig.host,
2886
- port: this.psqlConnectionPool.poolConfig.port
2887
- });
2888
- const promises = [originalClient.connect(), scratchClient.connect()];
2889
- await Promise.all(promises);
2890
- const infoPromises = [pgInfo({ client: originalClient }), pgInfo({ client: scratchClient })];
2891
- const [info1, info2] = await Promise.all(infoPromises);
2892
- const diff = getDiff(info1, info2);
2893
- const endPromises = [originalClient.end(), scratchClient.end()];
2894
- await Promise.all(endPromises);
2895
- return diff.join("\n");
3027
+ return diffDatabaseToSchema(schema, this.psqlConnectionPool, this.scratchDbName);
2896
3028
  }
2897
3029
  createNestedSelect(req, schema, item, routeData, sqlParams) {
2898
3030
  if (!item.subquery) return "";
@@ -3156,316 +3288,98 @@ DELETE FROM "${routeData.table}" ${joinStatement} ${whereClause}`;
3156
3288
  });
3157
3289
  return joinStatements;
3158
3290
  }
3159
- generateGroupBy(routeData) {
3160
- let groupBy = "";
3161
- if (routeData.groupBy) {
3162
- groupBy = `GROUP BY ${escapeColumnName(routeData.groupBy.tableName)}.${escapeColumnName(routeData.groupBy.columnName)}
3163
- `;
3164
- }
3165
- return groupBy;
3166
- }
3167
- generateOrderBy(req, routeData) {
3168
- let orderBy = "";
3169
- const orderOptions = {
3170
- ASC: "ASC",
3171
- DESC: "DESC"
3172
- };
3173
- const data = req.data;
3174
- if (routeData.type === "PAGED" && "sortBy" in data) {
3175
- const sortOrder = orderOptions[data.sortOrder] || "ASC";
3176
- orderBy = `ORDER BY ${escapeColumnName(data.sortBy)} ${sortOrder}
3177
- `;
3178
- } else if (routeData.orderBy) {
3179
- const sortOrder = orderOptions[routeData.orderBy.order] || "ASC";
3180
- orderBy = `ORDER BY ${escapeColumnName(routeData.orderBy.tableName)}.${escapeColumnName(routeData.orderBy.columnName)} ${sortOrder}
3181
- `;
3182
- }
3183
- return orderBy;
3184
- }
3185
- generateWhereClause(req, where, routeData, sqlParams) {
3186
- let whereClause = "";
3187
- where.forEach((item, index) => {
3188
- if (index === 0) whereClause = "WHERE ";
3189
- if (item.custom) {
3190
- const customReplaced = this.replaceParamKeywords(item.custom, routeData, req, sqlParams);
3191
- whereClause += ` ${item.conjunction || ""} ${customReplaced}
3192
- `;
3193
- return;
3194
- }
3195
- if (item.operator === void 0 || item.value === void 0 || item.columnName === void 0 || item.tableName === void 0)
3196
- throw new RsError(
3197
- "SCHEMA_ERROR",
3198
- `Invalid where clause in route ${routeData.name}, missing required fields if not custom`
3199
- );
3200
- let operator = item.operator;
3201
- let value = item.value;
3202
- if (operator === "LIKE") {
3203
- value = `'%' || ${value} || '%'`;
3204
- } else if (operator === "NOT LIKE") {
3205
- value = `'%' || ${value} || '%'`;
3206
- } else if (operator === "STARTS WITH") {
3207
- operator = "LIKE";
3208
- value = `${value} || '%'`;
3209
- } else if (operator === "ENDS WITH") {
3210
- operator = "LIKE";
3211
- value = `'%' || ${value}`;
3212
- }
3213
- const replacedValue = this.replaceParamKeywords(value, routeData, req, sqlParams);
3214
- whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator.replace("LIKE", "ILIKE")} ${["IN", "NOT IN"].includes(operator) ? `(${replacedValue})` : replacedValue}
3215
- `;
3216
- });
3217
- const data = req.data;
3218
- if (routeData.type === "PAGED" && !!data?.filter) {
3219
- let statement = data.filter.replace(/\$[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
3220
- const requestParam = routeData.request.find((item) => {
3221
- return item.name === value.replace("$", "");
3222
- });
3223
- if (!requestParam)
3224
- throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
3225
- return data[requestParam.name]?.toString() || "";
3226
- });
3227
- statement = statement.replace(/#[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
3228
- const requestParam = routeData.request.find((item) => {
3229
- return item.name === value.replace("#", "");
3230
- });
3231
- if (!requestParam)
3232
- throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
3233
- return data[requestParam.name]?.toString() || "";
3234
- });
3235
- const parseResult = filterPsqlParser_default.parse(statement);
3236
- if (parseResult.usedOldSyntax) {
3237
- logger.warn(
3238
- `Deprecated filter syntax detected in route "${routeData.name}" (${routeData.path}). Please migrate to the new filter syntax.`
3239
- );
3240
- }
3241
- statement = parseResult.sql;
3242
- if (whereClause.startsWith("WHERE")) {
3243
- whereClause += ` AND (${statement})
3244
- `;
3245
- } else {
3246
- whereClause += `WHERE ${statement}
3247
- `;
3248
- }
3249
- }
3250
- return whereClause;
3251
- }
3252
- createUpdateTrigger(tableName, notify) {
3253
- if (!notify) return "";
3254
- if (notify === "ALL") {
3255
- return `
3256
- CREATE OR REPLACE FUNCTION notify_${tableName}_update()
3257
- RETURNS TRIGGER AS $$
3258
- DECLARE
3259
- query_metadata JSON;
3260
- BEGIN
3261
- SELECT INTO query_metadata
3262
- (regexp_match(
3263
- current_query(),
3264
- '^--QUERY_METADATA\\(({.*})', 'n'
3265
- ))[1]::json;
3266
-
3267
- PERFORM pg_notify(
3268
- 'update',
3269
- json_build_object(
3270
- 'table', '${tableName}',
3271
- 'queryMetadata', query_metadata,
3272
- 'changedId', NEW.id,
3273
- 'record', NEW,
3274
- 'previousRecord', OLD
3275
- )::text
3276
- );
3277
- RETURN NEW;
3278
- END;
3279
- $$ LANGUAGE plpgsql;
3280
-
3281
- CREATE OR REPLACE TRIGGER ${tableName}_update
3282
- AFTER UPDATE ON "${tableName}"
3283
- FOR EACH ROW
3284
- EXECUTE FUNCTION notify_${tableName}_update();
3285
- `;
3286
- }
3287
- const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
3288
- const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
3289
- return `
3290
- CREATE OR REPLACE FUNCTION notify_${tableName}_update()
3291
- RETURNS TRIGGER AS $$
3292
- DECLARE
3293
- query_metadata JSON;
3294
- BEGIN
3295
- SELECT INTO query_metadata
3296
- (regexp_match(
3297
- current_query(),
3298
- '^--QUERY_METADATA\\(({.*})', 'n'
3299
- ))[1]::json;
3300
-
3301
- PERFORM pg_notify(
3302
- 'update',
3303
- json_build_object(
3304
- 'table', '${tableName}',
3305
- 'queryMetadata', query_metadata,
3306
- 'changedId', NEW.id,
3307
- 'record', json_build_object(
3308
- ${notifyColumnNewBuildString}
3309
- ),
3310
- 'previousRecord', json_build_object(
3311
- ${notifyColumnOldBuildString}
3312
- )
3313
- )::text
3314
- );
3315
- RETURN NEW;
3316
- END;
3317
- $$ LANGUAGE plpgsql;
3318
-
3319
- CREATE OR REPLACE TRIGGER ${tableName}_update
3320
- AFTER UPDATE ON "${tableName}"
3321
- FOR EACH ROW
3322
- EXECUTE FUNCTION notify_${tableName}_update();
3323
- `;
3324
- }
3325
- createDeleteTrigger(tableName, notify) {
3326
- if (!notify) return "";
3327
- if (notify === "ALL") {
3328
- return `
3329
- CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
3330
- RETURNS TRIGGER AS $$
3331
- DECLARE
3332
- query_metadata JSON;
3333
- BEGIN
3334
- SELECT INTO query_metadata
3335
- (regexp_match(
3336
- current_query(),
3337
- '^--QUERY_METADATA\\(({.*})', 'n'
3338
- ))[1]::json;
3339
-
3340
- PERFORM pg_notify(
3341
- 'delete',
3342
- json_build_object(
3343
- 'table', '${tableName}',
3344
- 'queryMetadata', query_metadata,
3345
- 'deletedId', OLD.id,
3346
- 'previousRecord', OLD
3347
- )::text
3348
- );
3349
- RETURN NEW;
3350
- END;
3351
- $$ LANGUAGE plpgsql;
3352
-
3353
- CREATE OR REPLACE TRIGGER "${tableName}_delete"
3354
- AFTER DELETE ON "${tableName}"
3355
- FOR EACH ROW
3356
- EXECUTE FUNCTION notify_${tableName}_delete();
3357
- `;
3358
- }
3359
- const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
3360
- return `
3361
- CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
3362
- RETURNS TRIGGER AS $$
3363
- DECLARE
3364
- query_metadata JSON;
3365
- BEGIN
3366
- SELECT INTO query_metadata
3367
- (regexp_match(
3368
- current_query(),
3369
- '^--QUERY_METADATA\\(({.*})', 'n'
3370
- ))[1]::json;
3371
-
3372
- PERFORM pg_notify(
3373
- 'delete',
3374
- json_build_object(
3375
- 'table', '${tableName}',
3376
- 'queryMetadata', query_metadata,
3377
- 'deletedId', OLD.id,
3378
- 'previousRecord', json_build_object(
3379
- ${notifyColumnOldBuildString}
3380
- )
3381
- )::text
3382
- );
3383
- RETURN NEW;
3384
- END;
3385
- $$ LANGUAGE plpgsql;
3386
-
3387
- CREATE OR REPLACE TRIGGER "${tableName}_delete"
3388
- AFTER DELETE ON "${tableName}"
3389
- FOR EACH ROW
3390
- EXECUTE FUNCTION notify_${tableName}_delete();
3391
- `;
3392
- }
3393
- createInsertTriggers(tableName, notify) {
3394
- if (!notify) return "";
3395
- if (notify === "ALL") {
3396
- return `
3397
- CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
3398
- RETURNS TRIGGER AS $$
3399
- DECLARE
3400
- query_metadata JSON;
3401
- BEGIN
3402
- SELECT INTO query_metadata
3403
- (regexp_match(
3404
- current_query(),
3405
- '^--QUERY_METADATA\\(({.*})', 'n'
3406
- ))[1]::json;
3407
-
3408
- PERFORM pg_notify(
3409
- 'insert',
3410
- json_build_object(
3411
- 'table', '${tableName}',
3412
- 'queryMetadata', query_metadata,
3413
- 'insertedId', NEW.id,
3414
- 'record', NEW
3415
- )::text
3416
- );
3417
-
3418
- RETURN NEW;
3419
- END;
3420
- $$ LANGUAGE plpgsql;
3421
-
3422
- CREATE OR REPLACE TRIGGER "${tableName}_insert"
3423
- AFTER INSERT ON "${tableName}"
3424
- FOR EACH ROW
3425
- EXECUTE FUNCTION notify_${tableName}_insert();
3291
+ generateGroupBy(routeData) {
3292
+ let groupBy = "";
3293
+ if (routeData.groupBy) {
3294
+ groupBy = `GROUP BY ${escapeColumnName(routeData.groupBy.tableName)}.${escapeColumnName(routeData.groupBy.columnName)}
3426
3295
  `;
3427
3296
  }
3428
- const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
3429
- return `
3430
- CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
3431
- RETURNS TRIGGER AS $$
3432
- DECLARE
3433
- query_metadata JSON;
3434
- BEGIN
3435
- SELECT INTO query_metadata
3436
- (regexp_match(
3437
- current_query(),
3438
- '^--QUERY_METADATA\\(({.*})', 'n'
3439
- ))[1]::json;
3440
-
3441
- PERFORM pg_notify(
3442
- 'insert',
3443
- json_build_object(
3444
- 'table', '${tableName}',
3445
- 'queryMetadata', query_metadata,
3446
- 'insertedId', NEW.id,
3447
- 'record', json_build_object(
3448
- ${notifyColumnNewBuildString}
3449
- )
3450
- )::text
3451
- );
3452
-
3453
- RETURN NEW;
3454
- END;
3455
- $$ LANGUAGE plpgsql;
3456
-
3457
- CREATE OR REPLACE TRIGGER "${tableName}_insert"
3458
- AFTER INSERT ON "${tableName}"
3459
- FOR EACH ROW
3460
- EXECUTE FUNCTION notify_${tableName}_insert();
3297
+ return groupBy;
3298
+ }
3299
+ generateOrderBy(req, routeData) {
3300
+ let orderBy = "";
3301
+ const orderOptions = {
3302
+ ASC: "ASC",
3303
+ DESC: "DESC"
3304
+ };
3305
+ const data = req.data;
3306
+ if (routeData.type === "PAGED" && "sortBy" in data) {
3307
+ const sortOrder = orderOptions[data.sortOrder] || "ASC";
3308
+ orderBy = `ORDER BY ${escapeColumnName(data.sortBy)} ${sortOrder}
3309
+ `;
3310
+ } else if (routeData.orderBy) {
3311
+ const sortOrder = orderOptions[routeData.orderBy.order] || "ASC";
3312
+ orderBy = `ORDER BY ${escapeColumnName(routeData.orderBy.tableName)}.${escapeColumnName(routeData.orderBy.columnName)} ${sortOrder}
3461
3313
  `;
3314
+ }
3315
+ return orderBy;
3462
3316
  }
3463
- schemaToPsqlType(column) {
3464
- if (column.hasAutoIncrement) return "BIGSERIAL";
3465
- if (column.type === "ENUM") return `TEXT`;
3466
- if (column.type === "DATETIME") return "TIMESTAMPTZ";
3467
- if (column.type === "MEDIUMINT") return "INT";
3468
- return column.type;
3317
+ generateWhereClause(req, where, routeData, sqlParams) {
3318
+ let whereClause = "";
3319
+ where.forEach((item, index) => {
3320
+ if (index === 0) whereClause = "WHERE ";
3321
+ if (item.custom) {
3322
+ const customReplaced = this.replaceParamKeywords(item.custom, routeData, req, sqlParams);
3323
+ whereClause += ` ${item.conjunction || ""} ${customReplaced}
3324
+ `;
3325
+ return;
3326
+ }
3327
+ if (item.operator === void 0 || item.value === void 0 || item.columnName === void 0 || item.tableName === void 0)
3328
+ throw new RsError(
3329
+ "SCHEMA_ERROR",
3330
+ `Invalid where clause in route ${routeData.name}, missing required fields if not custom`
3331
+ );
3332
+ let operator = item.operator;
3333
+ let value = item.value;
3334
+ if (operator === "LIKE") {
3335
+ value = `'%' || ${value} || '%'`;
3336
+ } else if (operator === "NOT LIKE") {
3337
+ value = `'%' || ${value} || '%'`;
3338
+ } else if (operator === "STARTS WITH") {
3339
+ operator = "LIKE";
3340
+ value = `${value} || '%'`;
3341
+ } else if (operator === "ENDS WITH") {
3342
+ operator = "LIKE";
3343
+ value = `'%' || ${value}`;
3344
+ }
3345
+ const replacedValue = this.replaceParamKeywords(value, routeData, req, sqlParams);
3346
+ whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator.replace("LIKE", "ILIKE")} ${["IN", "NOT IN"].includes(operator) ? `(${replacedValue})` : replacedValue}
3347
+ `;
3348
+ });
3349
+ const data = req.data;
3350
+ if (routeData.type === "PAGED" && !!data?.filter) {
3351
+ let statement = data.filter.replace(/\$[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
3352
+ const requestParam = routeData.request.find((item) => {
3353
+ return item.name === value.replace("$", "");
3354
+ });
3355
+ if (!requestParam)
3356
+ throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
3357
+ return data[requestParam.name]?.toString() || "";
3358
+ });
3359
+ statement = statement.replace(/#[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
3360
+ const requestParam = routeData.request.find((item) => {
3361
+ return item.name === value.replace("#", "");
3362
+ });
3363
+ if (!requestParam)
3364
+ throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
3365
+ return data[requestParam.name]?.toString() || "";
3366
+ });
3367
+ const parseResult = filterPsqlParser_default.parse(statement);
3368
+ if (parseResult.usedOldSyntax) {
3369
+ logger.warn(
3370
+ `Deprecated filter syntax detected in route "${routeData.name}" (${routeData.path}). Please migrate to the new filter syntax.`
3371
+ );
3372
+ }
3373
+ statement = parseResult.sql;
3374
+ if (whereClause.startsWith("WHERE")) {
3375
+ whereClause += ` AND (${statement})
3376
+ `;
3377
+ } else {
3378
+ whereClause += `WHERE ${statement}
3379
+ `;
3380
+ }
3381
+ }
3382
+ return whereClause;
3469
3383
  }
3470
3384
  };
3471
3385
 
@@ -3542,8 +3456,9 @@ var ResturaEngine = class {
3542
3456
  * @param app - The Express application instance to initialize with Restura.
3543
3457
  * @returns A promise that resolves when the initialization is complete.
3544
3458
  */
3545
- async init(app, authenticationHandler, psqlConnectionPool) {
3546
- this.resturaConfig = await config2.validate("restura", resturaConfigSchema);
3459
+ async init(app, authenticationHandler, psqlConnectionPool, options) {
3460
+ if (options?.logger) setLogger(options.logger);
3461
+ this.resturaConfig = await config.validate("restura", resturaConfigSchema);
3547
3462
  this.multerCommonUpload = getMulterUpload(this.resturaConfig.fileTempCachePath);
3548
3463
  new TempCache(this.resturaConfig.fileTempCachePath);
3549
3464
  this.psqlConnectionPool = psqlConnectionPool;
@@ -3897,14 +3812,556 @@ __decorateClass([
3897
3812
  ], ResturaEngine.prototype, "runCustomRouteLogic", 1);
3898
3813
  var restura = new ResturaEngine();
3899
3814
 
3815
+ // src/restura/sql/psqlIntrospect.ts
3816
+ var RESTURA_TO_PG_UDT = {
3817
+ BIGSERIAL: "int8",
3818
+ SERIAL: "int4",
3819
+ BIGINT: "int8",
3820
+ INTEGER: "int4",
3821
+ INT: "int4",
3822
+ SMALLINT: "int2",
3823
+ DECIMAL: "numeric",
3824
+ NUMERIC: "numeric",
3825
+ REAL: "float4",
3826
+ "DOUBLE PRECISION": "float8",
3827
+ FLOAT: "float8",
3828
+ DOUBLE: "float8",
3829
+ BOOLEAN: "bool",
3830
+ TEXT: "text",
3831
+ VARCHAR: "varchar",
3832
+ CHAR: "bpchar",
3833
+ BYTEA: "bytea",
3834
+ JSON: "json",
3835
+ JSONB: "jsonb",
3836
+ DATE: "date",
3837
+ TIME: "time",
3838
+ TIMESTAMP: "timestamp",
3839
+ TIMESTAMPTZ: "timestamptz",
3840
+ INTERVAL: "interval",
3841
+ ENUM: "text",
3842
+ DATETIME: "timestamptz",
3843
+ MEDIUMINT: "int4",
3844
+ TINYINT: "int2"
3845
+ };
3846
+ function resturaTypeToUdt(column) {
3847
+ const psqlType = schemaToPsqlType(column);
3848
+ return RESTURA_TO_PG_UDT[psqlType] ?? psqlType.toLowerCase();
3849
+ }
3850
+ var PG_FK_ACTION = {
3851
+ a: "NO ACTION",
3852
+ r: "RESTRICT",
3853
+ c: "CASCADE",
3854
+ n: "SET NULL",
3855
+ d: "SET DEFAULT"
3856
+ };
3857
+ async function introspectDatabase(pool) {
3858
+ const [tableRows, columnRows, indexRows, fkRows, checkRows] = await Promise.all([
3859
+ pool.runQuery(
3860
+ `SELECT table_name
3861
+ FROM information_schema.tables
3862
+ WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
3863
+ ORDER BY table_name`,
3864
+ [],
3865
+ systemUser
3866
+ ),
3867
+ pool.runQuery(
3868
+ `SELECT table_name, column_name, udt_name, is_nullable, column_default,
3869
+ character_maximum_length, numeric_precision, numeric_scale
3870
+ FROM information_schema.columns
3871
+ WHERE table_schema = 'public'
3872
+ ORDER BY table_name, ordinal_position`,
3873
+ [],
3874
+ systemUser
3875
+ ),
3876
+ pool.runQuery(
3877
+ `SELECT pi.tablename, pi.indexname, pi.indexdef, ix.indisprimary
3878
+ FROM pg_indexes pi
3879
+ JOIN pg_class ic ON ic.relname = pi.indexname
3880
+ JOIN pg_index ix ON ix.indexrelid = ic.oid
3881
+ WHERE pi.schemaname = 'public'
3882
+ ORDER BY pi.tablename, pi.indexname`,
3883
+ [],
3884
+ systemUser
3885
+ ),
3886
+ pool.runQuery(
3887
+ `SELECT
3888
+ constraint_def.conname AS constraint_name,
3889
+ source_table.relname AS table_name,
3890
+ source_column.attname AS column_name,
3891
+ referenced_table.relname AS ref_table,
3892
+ referenced_column.attname AS ref_column,
3893
+ constraint_def.confdeltype AS delete_rule,
3894
+ constraint_def.confupdtype AS update_rule
3895
+ FROM pg_constraint constraint_def
3896
+ JOIN pg_class source_table ON source_table.oid = constraint_def.conrelid
3897
+ JOIN pg_namespace schema_ns ON schema_ns.oid = source_table.relnamespace
3898
+ JOIN pg_class referenced_table ON referenced_table.oid = constraint_def.confrelid
3899
+ JOIN pg_attribute source_column ON source_column.attrelid = constraint_def.conrelid AND source_column.attnum = ANY(constraint_def.conkey)
3900
+ JOIN pg_attribute referenced_column ON referenced_column.attrelid = constraint_def.confrelid AND referenced_column.attnum = ANY(constraint_def.confkey)
3901
+ WHERE constraint_def.contype = 'f' AND schema_ns.nspname = 'public'
3902
+ ORDER BY source_table.relname, constraint_def.conname`,
3903
+ [],
3904
+ systemUser
3905
+ ),
3906
+ pool.runQuery(
3907
+ `SELECT
3908
+ constraint_def.conname AS constraint_name,
3909
+ parent_table.relname AS table_name,
3910
+ pg_get_constraintdef(constraint_def.oid) AS check_clause
3911
+ FROM pg_constraint constraint_def
3912
+ JOIN pg_class parent_table ON parent_table.oid = constraint_def.conrelid
3913
+ JOIN pg_namespace schema_ns ON schema_ns.oid = parent_table.relnamespace
3914
+ WHERE constraint_def.contype = 'c' AND schema_ns.nspname = 'public'
3915
+ AND constraint_def.conname NOT LIKE '%_not_null'
3916
+ ORDER BY parent_table.relname, constraint_def.conname`,
3917
+ [],
3918
+ systemUser
3919
+ )
3920
+ ]);
3921
+ const tableMap = /* @__PURE__ */ new Map();
3922
+ for (const row of tableRows) {
3923
+ tableMap.set(row.table_name, {
3924
+ name: row.table_name,
3925
+ columns: [],
3926
+ indexes: [],
3927
+ foreignKeys: [],
3928
+ checkConstraints: []
3929
+ });
3930
+ }
3931
+ for (const row of columnRows) {
3932
+ const table = tableMap.get(row.table_name);
3933
+ if (!table) continue;
3934
+ table.columns.push({
3935
+ name: row.column_name,
3936
+ udtName: row.udt_name,
3937
+ isNullable: row.is_nullable === "YES",
3938
+ columnDefault: row.column_default,
3939
+ characterMaximumLength: row.character_maximum_length,
3940
+ numericPrecision: row.numeric_precision,
3941
+ numericScale: row.numeric_scale
3942
+ });
3943
+ }
3944
+ for (const row of indexRows) {
3945
+ const table = tableMap.get(row.tablename);
3946
+ if (!table) continue;
3947
+ const isPrimary = row.indisprimary;
3948
+ const unique = /CREATE UNIQUE INDEX/i.test(row.indexdef);
3949
+ const order = row.indexdef.toUpperCase().includes(" DESC") ? "DESC" : "ASC";
3950
+ const columnMatch = row.indexdef.match(/\((.+?)\)(?:\s+WHERE\s+(.+))?$/i);
3951
+ const columns = columnMatch ? columnMatch[1].split(",").map(
3952
+ (colExpr) => colExpr.trim().replace(/^"(.*)"$/, "$1").replace(/\s+(ASC|DESC)$/i, "")
3953
+ ) : [];
3954
+ const whereClause = columnMatch?.[2] ?? null;
3955
+ table.indexes.push({
3956
+ name: row.indexname,
3957
+ tableName: row.tablename,
3958
+ isUnique: unique,
3959
+ isPrimary,
3960
+ columns,
3961
+ order,
3962
+ where: whereClause
3963
+ });
3964
+ }
3965
+ for (const row of fkRows) {
3966
+ const table = tableMap.get(row.table_name);
3967
+ if (!table) continue;
3968
+ table.foreignKeys.push({
3969
+ name: row.constraint_name,
3970
+ tableName: row.table_name,
3971
+ column: row.column_name,
3972
+ refTable: row.ref_table,
3973
+ refColumn: row.ref_column,
3974
+ onDelete: PG_FK_ACTION[row.delete_rule] ?? "NO ACTION",
3975
+ onUpdate: PG_FK_ACTION[row.update_rule] ?? "NO ACTION"
3976
+ });
3977
+ }
3978
+ for (const row of checkRows) {
3979
+ const table = tableMap.get(row.table_name);
3980
+ if (!table) continue;
3981
+ table.checkConstraints.push({
3982
+ name: row.constraint_name,
3983
+ tableName: row.table_name,
3984
+ expression: row.check_clause
3985
+ });
3986
+ }
3987
+ return { tables: Array.from(tableMap.values()) };
3988
+ }
3989
+ function diffSchemaToDatabase(schema, snapshot) {
3990
+ const statements = [];
3991
+ const desiredTables = new Map(schema.database.map((table) => [table.name, table]));
3992
+ const liveTableMap = new Map(snapshot.tables.map((table) => [table.name, table]));
3993
+ const tablesToCreate = schema.database.filter((table) => !liveTableMap.has(table.name));
3994
+ const tablesToDrop = snapshot.tables.filter((table) => !desiredTables.has(table.name));
3995
+ const tablesToAlter = schema.database.filter((table) => liveTableMap.has(table.name));
3996
+ const changedChecksPerTable = /* @__PURE__ */ new Map();
3997
+ for (const desired of tablesToAlter) {
3998
+ const live = liveTableMap.get(desired.name);
3999
+ const desiredFkNames = new Set(desired.foreignKeys.map((fk) => fk.name));
4000
+ for (const liveFk of live.foreignKeys) {
4001
+ if (!desiredFkNames.has(liveFk.name) || isFkChanged(desired, liveFk)) {
4002
+ statements.push(`ALTER TABLE "${desired.name}" DROP CONSTRAINT "${liveFk.name}";`);
4003
+ }
4004
+ }
4005
+ const desiredCheckExprMap = /* @__PURE__ */ new Map();
4006
+ for (const check of desired.checkConstraints) {
4007
+ desiredCheckExprMap.set(check.name, check.check);
4008
+ }
4009
+ for (const col of desired.columns) {
4010
+ if (col.type === "ENUM" && col.value) {
4011
+ desiredCheckExprMap.set(`${desired.name}_${col.name}_check`, `"${col.name}" IN (${col.value})`);
4012
+ }
4013
+ }
4014
+ const changedChecks = /* @__PURE__ */ new Set();
4015
+ for (const liveCheck of live.checkConstraints) {
4016
+ if (!desiredCheckExprMap.has(liveCheck.name)) {
4017
+ statements.push(`ALTER TABLE "${desired.name}" DROP CONSTRAINT "${liveCheck.name}";`);
4018
+ } else if (normalizeCheckExpression(desiredCheckExprMap.get(liveCheck.name)) !== normalizeCheckExpression(liveCheck.expression)) {
4019
+ statements.push(`ALTER TABLE "${desired.name}" DROP CONSTRAINT "${liveCheck.name}";`);
4020
+ changedChecks.add(liveCheck.name);
4021
+ }
4022
+ }
4023
+ changedChecksPerTable.set(desired.name, changedChecks);
4024
+ const desiredIdxSignatures = /* @__PURE__ */ new Map();
4025
+ for (const idx of desired.indexes) {
4026
+ if (idx.isPrimaryKey) continue;
4027
+ desiredIdxSignatures.set(idx.name, indexSignature(idx.name, idx.columns, idx.isUnique, idx.order, idx.where));
4028
+ }
4029
+ const autoUniqueNames = /* @__PURE__ */ new Set();
4030
+ for (const col of desired.columns) {
4031
+ if (col.isUnique) {
4032
+ autoUniqueNames.add(`${desired.name}_${col.name}_unique_index`);
4033
+ }
4034
+ }
4035
+ for (const liveIdx of live.indexes) {
4036
+ if (liveIdx.isPrimary) continue;
4037
+ if (autoUniqueNames.has(liveIdx.name)) continue;
4038
+ const liveSig = indexSignature(liveIdx.name, liveIdx.columns, liveIdx.isUnique, liveIdx.order, liveIdx.where);
4039
+ const desiredSig = desiredIdxSignatures.get(liveIdx.name);
4040
+ if (!desiredSig || desiredSig !== liveSig) {
4041
+ statements.push(`DROP INDEX "${liveIdx.name}";`);
4042
+ }
4043
+ }
4044
+ diffColumns(desired, live, statements);
4045
+ }
4046
+ for (const table of tablesToDrop) {
4047
+ statements.push(`DROP TABLE "${table.name}";`);
4048
+ }
4049
+ const { sorted: sortedTablesToCreate, deferredFkNames } = topologicalSortTables(tablesToCreate);
4050
+ for (const table of sortedTablesToCreate) {
4051
+ statements.push(buildCreateTable(table, deferredFkNames));
4052
+ }
4053
+ for (const table of sortedTablesToCreate) {
4054
+ for (const index of table.indexes) {
4055
+ if (!index.isPrimaryKey) {
4056
+ statements.push(buildCreateIndex(table.name, index));
4057
+ }
4058
+ }
4059
+ }
4060
+ for (const desired of tablesToAlter) {
4061
+ const live = liveTableMap.get(desired.name);
4062
+ const liveIdxSignatures = /* @__PURE__ */ new Map();
4063
+ for (const idx of live.indexes) {
4064
+ liveIdxSignatures.set(idx.name, indexSignature(idx.name, idx.columns, idx.isUnique, idx.order, idx.where));
4065
+ }
4066
+ for (const index of desired.indexes) {
4067
+ if (index.isPrimaryKey) continue;
4068
+ const desiredSig = indexSignature(index.name, index.columns, index.isUnique, index.order, index.where);
4069
+ const liveSig = liveIdxSignatures.get(index.name);
4070
+ if (!liveSig || liveSig !== desiredSig) {
4071
+ statements.push(buildCreateIndex(desired.name, index));
4072
+ }
4073
+ }
4074
+ }
4075
+ for (const table of sortedTablesToCreate) {
4076
+ for (const fk of table.foreignKeys) {
4077
+ if (deferredFkNames.has(fk.name)) {
4078
+ statements.push(buildAddForeignKey(table.name, fk));
4079
+ }
4080
+ }
4081
+ }
4082
+ for (const desired of tablesToAlter) {
4083
+ const live = liveTableMap.get(desired.name);
4084
+ const liveFkNames = new Set(live.foreignKeys.map((fk) => fk.name));
4085
+ for (const fk of desired.foreignKeys) {
4086
+ if (!liveFkNames.has(fk.name) || isFkChanged(
4087
+ desired,
4088
+ liveTableMap.get(desired.name).foreignKeys.find((liveFk) => liveFk.name === fk.name)
4089
+ )) {
4090
+ statements.push(buildAddForeignKey(desired.name, fk));
4091
+ }
4092
+ }
4093
+ }
4094
+ for (const desired of tablesToAlter) {
4095
+ const live = liveTableMap.get(desired.name);
4096
+ const liveCheckNames = new Set(live.checkConstraints.map((check) => check.name));
4097
+ const changedChecks = changedChecksPerTable.get(desired.name) ?? /* @__PURE__ */ new Set();
4098
+ for (const check of desired.checkConstraints) {
4099
+ if (!liveCheckNames.has(check.name) || changedChecks.has(check.name)) {
4100
+ statements.push(buildAddCheckConstraint(desired.name, check));
4101
+ }
4102
+ }
4103
+ for (const col of desired.columns) {
4104
+ if (col.type === "ENUM" && col.value) {
4105
+ const checkName = `${desired.name}_${col.name}_check`;
4106
+ if (!liveCheckNames.has(checkName) || changedChecks.has(checkName)) {
4107
+ statements.push(
4108
+ `ALTER TABLE "${desired.name}" ADD CONSTRAINT "${checkName}" CHECK ("${col.name}" IN (${col.value}));`
4109
+ );
4110
+ }
4111
+ }
4112
+ }
4113
+ }
4114
+ return statements;
4115
+ }
4116
+ function diffColumns(desired, live, statements) {
4117
+ const liveColMap = new Map(live.columns.map((col) => [col.name, col]));
4118
+ const desiredColNames = new Set(desired.columns.map((col) => col.name));
4119
+ for (const liveCol of live.columns) {
4120
+ if (!desiredColNames.has(liveCol.name)) {
4121
+ statements.push(`ALTER TABLE "${desired.name}" DROP COLUMN "${liveCol.name}";`);
4122
+ }
4123
+ }
4124
+ for (const column of desired.columns) {
4125
+ const liveColumn = liveColMap.get(column.name);
4126
+ if (!liveColumn) {
4127
+ statements.push(buildAddColumn(desired.name, column));
4128
+ continue;
4129
+ }
4130
+ const desiredUdt = resturaTypeToUdt(column);
4131
+ const udtMismatch = liveColumn.udtName !== desiredUdt && !isSerialMatch(column, liveColumn);
4132
+ if (udtMismatch || !udtMismatch && modifiersDiffer(column, liveColumn)) {
4133
+ const pgType = resturaTypeToPgCast(column);
4134
+ statements.push(
4135
+ `ALTER TABLE "${desired.name}" ALTER COLUMN "${column.name}" TYPE ${pgType} USING "${column.name}"::${pgType};`
4136
+ );
4137
+ }
4138
+ if (column.isNullable && !liveColumn.isNullable) {
4139
+ statements.push(`ALTER TABLE "${desired.name}" ALTER COLUMN "${column.name}" DROP NOT NULL;`);
4140
+ } else if (!column.isNullable && liveColumn.isNullable) {
4141
+ statements.push(`ALTER TABLE "${desired.name}" ALTER COLUMN "${column.name}" SET NOT NULL;`);
4142
+ }
4143
+ const desiredDefault = getDesiredDefault(column);
4144
+ const liveDefault = liveColumn.columnDefault;
4145
+ if (!defaultsMatch(desiredDefault, liveDefault, column)) {
4146
+ if (desiredDefault === null) {
4147
+ statements.push(`ALTER TABLE "${desired.name}" ALTER COLUMN "${column.name}" DROP DEFAULT;`);
4148
+ } else {
4149
+ statements.push(
4150
+ `ALTER TABLE "${desired.name}" ALTER COLUMN "${column.name}" SET DEFAULT ${desiredDefault};`
4151
+ );
4152
+ }
4153
+ }
4154
+ }
4155
+ }
4156
+ function topologicalSortTables(tables) {
4157
+ const tableNames = new Set(tables.map((t) => t.name));
4158
+ const tableMap = new Map(tables.map((t) => [t.name, t]));
4159
+ const inDegree = /* @__PURE__ */ new Map();
4160
+ const tableDeps = /* @__PURE__ */ new Map();
4161
+ const reverseDeps = /* @__PURE__ */ new Map();
4162
+ for (const table of tables) {
4163
+ inDegree.set(table.name, 0);
4164
+ tableDeps.set(table.name, /* @__PURE__ */ new Set());
4165
+ reverseDeps.set(table.name, /* @__PURE__ */ new Set());
4166
+ }
4167
+ for (const table of tables) {
4168
+ for (const fk of table.foreignKeys) {
4169
+ if (tableNames.has(fk.refTable) && fk.refTable !== table.name && !tableDeps.get(table.name).has(fk.refTable)) {
4170
+ tableDeps.get(table.name).add(fk.refTable);
4171
+ inDegree.set(table.name, (inDegree.get(table.name) ?? 0) + 1);
4172
+ reverseDeps.get(fk.refTable).add(table.name);
4173
+ }
4174
+ }
4175
+ }
4176
+ const queue = [];
4177
+ for (const [name, degree] of inDegree) {
4178
+ if (degree === 0) queue.push(name);
4179
+ }
4180
+ const sorted = [];
4181
+ while (queue.length > 0) {
4182
+ const name = queue.shift();
4183
+ sorted.push(name);
4184
+ for (const dependent of reverseDeps.get(name) ?? []) {
4185
+ const newDegree = (inDegree.get(dependent) ?? 1) - 1;
4186
+ inDegree.set(dependent, newDegree);
4187
+ if (newDegree === 0) queue.push(dependent);
4188
+ }
4189
+ }
4190
+ const sortedSet = new Set(sorted);
4191
+ const deferredFkNames = /* @__PURE__ */ new Set();
4192
+ const cycleTables = tables.filter((t) => !sortedSet.has(t.name));
4193
+ const placed = new Set(sorted);
4194
+ for (const table of cycleTables) {
4195
+ sorted.push(table.name);
4196
+ for (const fk of table.foreignKeys) {
4197
+ if (fk.refTable !== table.name && tableNames.has(fk.refTable) && !placed.has(fk.refTable)) {
4198
+ deferredFkNames.add(fk.name);
4199
+ }
4200
+ }
4201
+ placed.add(table.name);
4202
+ }
4203
+ return { sorted: sorted.map((name) => tableMap.get(name)), deferredFkNames };
4204
+ }
4205
+ function buildCreateTable(table, deferredFkNames = /* @__PURE__ */ new Set()) {
4206
+ const definitions = [];
4207
+ for (const column of table.columns) {
4208
+ let definition = `"${column.name}" ${buildColumnType(column)}`;
4209
+ if (column.isPrimary) definition += " PRIMARY KEY";
4210
+ if (column.isUnique) definition += ` CONSTRAINT "${table.name}_${column.name}_unique_index" UNIQUE`;
4211
+ if (!column.isNullable) definition += " NOT NULL";
4212
+ else definition += " NULL";
4213
+ if (column.default) definition += ` DEFAULT ${column.default}`;
4214
+ definitions.push(definition);
4215
+ }
4216
+ for (const fk of table.foreignKeys) {
4217
+ if (deferredFkNames.has(fk.name)) continue;
4218
+ definitions.push(
4219
+ `CONSTRAINT "${fk.name}" FOREIGN KEY ("${fk.column}") REFERENCES "${fk.refTable}" ("${fk.refColumn}") ON DELETE ${fk.onDelete} ON UPDATE ${fk.onUpdate}`
4220
+ );
4221
+ }
4222
+ for (const check of table.checkConstraints) {
4223
+ definitions.push(`CONSTRAINT "${check.name}" CHECK (${check.check})`);
4224
+ }
4225
+ for (const col of table.columns) {
4226
+ if (col.type === "ENUM" && col.value) {
4227
+ definitions.push(
4228
+ `CONSTRAINT "${table.name}_${col.name}_check" CHECK ("${col.name}" IN (${col.value}))`
4229
+ );
4230
+ }
4231
+ }
4232
+ return `CREATE TABLE "${table.name}" (
4233
+ ${definitions.join(",\n ")}
4234
+ );`;
4235
+ }
4236
+ function buildColumnType(column) {
4237
+ const baseType = schemaToPsqlType(column);
4238
+ let value = column.value;
4239
+ if (column.type === "JSON" || column.type === "JSONB") value = "";
4240
+ if (column.type === "DECIMAL" && value) {
4241
+ value = value.replace("-", ",").replace(/['"]/g, "");
4242
+ }
4243
+ if (value && column.type !== "ENUM") {
4244
+ return `${baseType}(${value})`;
4245
+ }
4246
+ if (column.length) return `${baseType}(${column.length})`;
4247
+ return baseType;
4248
+ }
4249
+ function buildAddColumn(tableName, column) {
4250
+ let definition = `ALTER TABLE "${tableName}" ADD COLUMN "${column.name}" ${buildColumnType(column)}`;
4251
+ if (column.isPrimary) definition += " PRIMARY KEY";
4252
+ if (column.isUnique) definition += ` CONSTRAINT "${tableName}_${column.name}_unique_index" UNIQUE`;
4253
+ if (!column.isNullable) definition += " NOT NULL";
4254
+ else definition += " NULL";
4255
+ if (column.default) definition += ` DEFAULT ${column.default}`;
4256
+ definition += ";";
4257
+ return definition;
4258
+ }
4259
+ function buildCreateIndex(tableName, index) {
4260
+ const unique = index.isUnique ? "UNIQUE " : "";
4261
+ let sql = `CREATE ${unique}INDEX "${index.name}" ON "${tableName}" (${index.columns.map((column) => `"${column}" ${index.order}`).join(", ")})`;
4262
+ if (index.where) sql += ` WHERE ${index.where}`;
4263
+ sql += ";";
4264
+ return sql;
4265
+ }
4266
+ function buildAddForeignKey(tableName, foreignKey) {
4267
+ return `ALTER TABLE "${tableName}" ADD CONSTRAINT "${foreignKey.name}" FOREIGN KEY ("${foreignKey.column}") REFERENCES "${foreignKey.refTable}" ("${foreignKey.refColumn}") ON DELETE ${foreignKey.onDelete} ON UPDATE ${foreignKey.onUpdate};`;
4268
+ }
4269
+ function buildAddCheckConstraint(tableName, constraint) {
4270
+ return `ALTER TABLE "${tableName}" ADD CONSTRAINT "${constraint.name}" CHECK (${constraint.check});`;
4271
+ }
4272
+ function indexSignature(name, columns, isUnique, order, where) {
4273
+ return `${name}|${columns.join(",")}|${isUnique}|${order}|${where || ""}`;
4274
+ }
4275
+ function isFkChanged(desired, liveFk) {
4276
+ const desiredFk = desired.foreignKeys.find((fk) => fk.name === liveFk.name);
4277
+ if (!desiredFk) return true;
4278
+ return desiredFk.column !== liveFk.column || desiredFk.refTable !== liveFk.refTable || desiredFk.refColumn !== liveFk.refColumn || desiredFk.onDelete !== liveFk.onDelete || desiredFk.onUpdate !== liveFk.onUpdate;
4279
+ }
4280
+ function normalizeCheckExpression(expr) {
4281
+ let s = expr;
4282
+ const checkMatch = s.match(/^CHECK\s*\(([\s\S]*)\)\s*$/i);
4283
+ if (checkMatch) s = checkMatch[1];
4284
+ s = s.replace(/::\w+(\[\])?/g, "");
4285
+ s = s.replace(/=\s*ANY\s*\(\s*\(?\s*ARRAY\s*\[([^\]]*)\]\s*\)?\s*\)/gi, "IN ($1)");
4286
+ s = s.replace(/"(\w+)"/g, "$1");
4287
+ s = s.replace(/\((\w+)\)/g, "$1");
4288
+ s = s.replace(/\s+/g, " ").trim().toLowerCase();
4289
+ while (s.startsWith("(") && s.endsWith(")")) {
4290
+ const inner = s.slice(1, -1);
4291
+ let depth = 0;
4292
+ let balanced = true;
4293
+ for (const char of inner) {
4294
+ if (char === "(") depth++;
4295
+ if (char === ")") depth--;
4296
+ if (depth < 0) {
4297
+ balanced = false;
4298
+ break;
4299
+ }
4300
+ }
4301
+ if (balanced && depth === 0) s = inner.trim();
4302
+ else break;
4303
+ }
4304
+ s = s.replace(/\s*,\s*/g, ", ");
4305
+ return s;
4306
+ }
4307
+ function isSerialMatch(column, liveCol) {
4308
+ if (column.hasAutoIncrement || column.type === "BIGSERIAL" || column.type === "SERIAL") {
4309
+ const serialUdt = column.type === "SERIAL" ? "int4" : "int8";
4310
+ return liveCol.udtName === serialUdt && liveCol.columnDefault?.startsWith("nextval(") === true;
4311
+ }
4312
+ return false;
4313
+ }
4314
+ function modifiersDiffer(column, liveColumn) {
4315
+ const desiredLength = column.length ?? null;
4316
+ if (desiredLength !== liveColumn.characterMaximumLength) return true;
4317
+ if (column.type === "DECIMAL" && column.value) {
4318
+ const parts = column.value.replace(/['"]/g, "").split("-");
4319
+ const desiredPrecision = parseInt(parts[0], 10);
4320
+ const desiredScale = parts.length > 1 ? parseInt(parts[1], 10) : 0;
4321
+ if (liveColumn.numericPrecision !== desiredPrecision || liveColumn.numericScale !== desiredScale) return true;
4322
+ }
4323
+ return false;
4324
+ }
4325
+ function resturaTypeToPgCast(column) {
4326
+ const baseType = schemaToPsqlType(column);
4327
+ const castMap = {
4328
+ BIGSERIAL: "BIGINT",
4329
+ SERIAL: "INTEGER",
4330
+ INT: "INTEGER",
4331
+ TIMESTAMPTZ: "TIMESTAMPTZ",
4332
+ TIMESTAMP: "TIMESTAMP",
4333
+ "DOUBLE PRECISION": "DOUBLE PRECISION"
4334
+ };
4335
+ let pgType = castMap[baseType] ?? baseType;
4336
+ let value = column.value;
4337
+ if (column.type === "DECIMAL" && value) {
4338
+ value = value.replace("-", ",").replace(/['"]/g, "");
4339
+ pgType = `${pgType}(${value})`;
4340
+ } else if (column.length) {
4341
+ pgType = `${pgType}(${column.length})`;
4342
+ }
4343
+ return pgType;
4344
+ }
4345
+ function getDesiredDefault(column) {
4346
+ if (column.hasAutoIncrement || column.type === "BIGSERIAL" || column.type === "SERIAL") return null;
4347
+ return column.default ?? null;
4348
+ }
4349
+ function defaultsMatch(desired, live, column) {
4350
+ if (column.hasAutoIncrement || column.type === "BIGSERIAL" || column.type === "SERIAL") return true;
4351
+ if (desired === null && live === null) return true;
4352
+ if (desired === null || live === null) return false;
4353
+ const normalizedLive = live.replace(/::[^\s]+$/g, "").trim();
4354
+ return desired.trim() === normalizedLive;
4355
+ }
4356
+
3900
4357
  // src/restura/sql/PsqlTransaction.ts
3901
- import pg3 from "pg";
3902
- var { Client: Client2 } = pg3;
4358
+ import pg4 from "pg";
4359
+ var { Client: Client3 } = pg4;
3903
4360
  var PsqlTransaction = class extends PsqlConnection {
3904
4361
  constructor(clientConfig, instanceId) {
3905
4362
  super(instanceId);
3906
4363
  this.clientConfig = clientConfig;
3907
- this.client = new Client2(clientConfig);
4364
+ this.client = new Client3(clientConfig);
3908
4365
  this.connectPromise = this.client.connect();
3909
4366
  this.beginTransactionPromise = this.beginTransaction();
3910
4367
  }
@@ -3944,10 +4401,18 @@ export {
3944
4401
  RsError,
3945
4402
  SQL,
3946
4403
  apiGenerator,
4404
+ createDeleteTriggerSql,
4405
+ createInsertTriggerSql,
4406
+ createUpdateTriggerSql,
4407
+ diffDatabaseToSchema,
4408
+ diffSchemaToDatabase,
3947
4409
  escapeColumnName,
3948
4410
  eventManager_default as eventManager,
3949
4411
  filterPsqlParser_default as filterPsqlParser,
4412
+ generateDatabaseSchemaFromSchema,
4413
+ getNewPublicSchemaAndScratchPool,
3950
4414
  insertObjectQuery,
4415
+ introspectDatabase,
3951
4416
  isSchemaValid,
3952
4417
  isValueNumber,
3953
4418
  logger,
@@ -3956,6 +4421,9 @@ export {
3956
4421
  restura,
3957
4422
  resturaGlobalTypesGenerator,
3958
4423
  resturaSchema,
4424
+ schemaToPsqlType,
4425
+ setLogger,
4426
+ systemUser,
3959
4427
  toSqlLiteral,
3960
4428
  updateObjectQuery
3961
4429
  };