@restura/core 1.9.2 → 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.d.ts +138 -42
- package/dist/index.js +1583 -1071
- package/dist/index.js.map +1 -1
- package/package.json +3 -6
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
var
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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 (
|
|
195
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
1290
|
+
import { z as z2 } from "zod";
|
|
1396
1291
|
|
|
1397
1292
|
// src/restura/schemas/validatorDataSchema.ts
|
|
1398
|
-
import { z
|
|
1399
|
-
var validatorDataSchemeValue =
|
|
1400
|
-
var validatorDataSchema =
|
|
1401
|
-
type:
|
|
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 =
|
|
1407
|
-
columnName:
|
|
1408
|
-
order:
|
|
1409
|
-
tableName:
|
|
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 =
|
|
1412
|
-
columnName:
|
|
1413
|
-
tableName:
|
|
1306
|
+
var groupBySchema = z2.object({
|
|
1307
|
+
columnName: z2.string(),
|
|
1308
|
+
tableName: z2.string()
|
|
1414
1309
|
}).strict();
|
|
1415
|
-
var whereDataSchema =
|
|
1416
|
-
tableName:
|
|
1417
|
-
columnName:
|
|
1418
|
-
operator:
|
|
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:
|
|
1435
|
-
custom:
|
|
1436
|
-
conjunction:
|
|
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 =
|
|
1439
|
-
name:
|
|
1440
|
-
value:
|
|
1333
|
+
var assignmentDataSchema = z2.object({
|
|
1334
|
+
name: z2.string(),
|
|
1335
|
+
value: z2.string()
|
|
1441
1336
|
}).strict();
|
|
1442
|
-
var joinDataSchema =
|
|
1443
|
-
table:
|
|
1444
|
-
localTable:
|
|
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:
|
|
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:
|
|
1449
|
-
foreignColumnName:
|
|
1450
|
-
custom:
|
|
1451
|
-
type:
|
|
1452
|
-
alias:
|
|
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 =
|
|
1455
|
-
name:
|
|
1456
|
-
required:
|
|
1457
|
-
isNullable:
|
|
1458
|
-
validator:
|
|
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 =
|
|
1461
|
-
name:
|
|
1462
|
-
selector:
|
|
1463
|
-
subquery:
|
|
1464
|
-
table:
|
|
1465
|
-
joins:
|
|
1466
|
-
where:
|
|
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
|
|
1363
|
+
return z2.array(responseDataSchema);
|
|
1469
1364
|
},
|
|
1470
1365
|
groupBy: groupBySchema.optional(),
|
|
1471
1366
|
orderBy: orderBySchema.optional()
|
|
1472
1367
|
}).optional(),
|
|
1473
|
-
type:
|
|
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 =
|
|
1477
|
-
method:
|
|
1478
|
-
name:
|
|
1479
|
-
description:
|
|
1480
|
-
path:
|
|
1481
|
-
deprecation:
|
|
1482
|
-
date:
|
|
1483
|
-
message:
|
|
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:
|
|
1486
|
-
scopes:
|
|
1380
|
+
roles: z2.array(z2.string()),
|
|
1381
|
+
scopes: z2.array(z2.string())
|
|
1487
1382
|
}).strict();
|
|
1488
1383
|
var standardRouteSchema = routeDataBaseSchema.extend({
|
|
1489
|
-
type:
|
|
1490
|
-
table:
|
|
1491
|
-
joins:
|
|
1492
|
-
assignments:
|
|
1493
|
-
where:
|
|
1494
|
-
request:
|
|
1495
|
-
response:
|
|
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:
|
|
1501
|
-
responseType:
|
|
1502
|
-
requestType:
|
|
1503
|
-
request:
|
|
1504
|
-
fileUploadType:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1611
|
-
name:
|
|
1612
|
-
type:
|
|
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:
|
|
1622
|
-
roles:
|
|
1623
|
-
scopes:
|
|
1624
|
-
comment:
|
|
1625
|
-
default:
|
|
1626
|
-
value:
|
|
1627
|
-
isPrimary:
|
|
1628
|
-
isUnique:
|
|
1629
|
-
hasAutoIncrement:
|
|
1630
|
-
length:
|
|
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 =
|
|
1633
|
-
name:
|
|
1634
|
-
columns:
|
|
1635
|
-
isUnique:
|
|
1636
|
-
isPrimaryKey:
|
|
1637
|
-
order:
|
|
1638
|
-
where:
|
|
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 =
|
|
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 =
|
|
1653
|
-
name:
|
|
1654
|
-
column:
|
|
1655
|
-
refTable:
|
|
1656
|
-
refColumn:
|
|
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 =
|
|
1661
|
-
name:
|
|
1662
|
-
check:
|
|
1555
|
+
var checkConstraintDataSchema = z2.object({
|
|
1556
|
+
name: z2.string(),
|
|
1557
|
+
check: z2.string()
|
|
1663
1558
|
}).strict();
|
|
1664
|
-
var tableDataSchema =
|
|
1665
|
-
name:
|
|
1666
|
-
columns:
|
|
1667
|
-
indexes:
|
|
1668
|
-
foreignKeys:
|
|
1669
|
-
checkConstraints:
|
|
1670
|
-
roles:
|
|
1671
|
-
scopes:
|
|
1672
|
-
notify:
|
|
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 =
|
|
1675
|
-
name:
|
|
1676
|
-
description:
|
|
1677
|
-
baseUrl:
|
|
1678
|
-
routes:
|
|
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 =
|
|
1681
|
-
database:
|
|
1682
|
-
endpoints:
|
|
1683
|
-
globalParams:
|
|
1684
|
-
roles:
|
|
1685
|
-
scopes:
|
|
1686
|
-
customTypes:
|
|
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
|
|
1588
|
+
if (error instanceof z2.ZodError) {
|
|
1694
1589
|
logger.error("Schema failed to validate with the following error:");
|
|
1695
|
-
console.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
|
|
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 =
|
|
1911
|
-
authToken:
|
|
1912
|
-
sendErrorStackTrace:
|
|
1913
|
-
schemaFilePath:
|
|
1914
|
-
customApiFolderPath:
|
|
1915
|
-
generatedTypesPath:
|
|
1916
|
-
fileTempCachePath:
|
|
1917
|
-
scratchDatabaseSuffix:
|
|
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
|
|
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 {
|
|
@@ -2530,7 +2286,15 @@ NullOperator
|
|
|
2530
2286
|
/ "null"i { return function(col) { return formatColumn(col) + ' IS NULL'; }; }
|
|
2531
2287
|
|
|
2532
2288
|
OperatorWithValue
|
|
2533
|
-
= "in"i _ "," _
|
|
2289
|
+
= "in"i _ "," _ vals:InValueList cast:TypeCast? { return function(col) {
|
|
2290
|
+
var formattedCol = formatColumn(col);
|
|
2291
|
+
var literals = vals.map(function(v) {
|
|
2292
|
+
var unescaped = unescapeValue(v);
|
|
2293
|
+
var formatted = formatValue(unescaped);
|
|
2294
|
+
return cast ? formatted + '::' + cast : formatted;
|
|
2295
|
+
});
|
|
2296
|
+
return formattedCol + ' IN (' + literals.join(', ') + ')';
|
|
2297
|
+
}; }
|
|
2534
2298
|
/ "ne"i _ "," _ val:CastedValue { return function(col) { return formatColumn(col) + ' <> ' + formatValueWithCast(val.value, val.cast); }; }
|
|
2535
2299
|
/ "gte"i _ "," _ val:CastedValue { return function(col) { return formatColumn(col) + ' >= ' + formatValueWithCast(val.value, val.cast); }; }
|
|
2536
2300
|
/ "gt"i _ "," _ val:CastedValue { return function(col) { return formatColumn(col) + ' > ' + formatValueWithCast(val.value, val.cast); }; }
|
|
@@ -2540,6 +2304,26 @@ OperatorWithValue
|
|
|
2540
2304
|
/ "sw"i _ "," _ val:CastedValue { return function(col) { var formatted = format.literal(unescapeValue(val.value) + '%'); return formatColumn(col) + ' ILIKE ' + (val.cast ? formatted + '::' + val.cast : formatted); }; }
|
|
2541
2305
|
/ "ew"i _ "," _ val:CastedValue { return function(col) { var formatted = format.literal('%' + unescapeValue(val.value)); return formatColumn(col) + ' ILIKE ' + (val.cast ? formatted + '::' + val.cast : formatted); }; }
|
|
2542
2306
|
|
|
2307
|
+
InValueList
|
|
2308
|
+
= first:InValue rest:("|" InValue)* {
|
|
2309
|
+
var values = [first];
|
|
2310
|
+
for (var i = 0; i < rest.length; i++) {
|
|
2311
|
+
values.push(rest[i][1]);
|
|
2312
|
+
}
|
|
2313
|
+
return values;
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
InValue
|
|
2317
|
+
= QuotedString
|
|
2318
|
+
/ chars:InValueChar+ { return chars.join(''); }
|
|
2319
|
+
|
|
2320
|
+
InValueChar
|
|
2321
|
+
= "\\\\\\\\" { return '\\\\\\\\'; }
|
|
2322
|
+
/ "\\\\," { return '\\\\,'; }
|
|
2323
|
+
/ "\\\\|" { return '\\\\|'; }
|
|
2324
|
+
/ [a-zA-Z0-9_\\-!#/@$%^&*+=<>?~.;'" ]
|
|
2325
|
+
/ c:":" !":" { return c; }
|
|
2326
|
+
|
|
2543
2327
|
CastedValue
|
|
2544
2328
|
= val:Value cast:TypeCast? { return { value: val, cast: cast }; }
|
|
2545
2329
|
|
|
@@ -2550,24 +2334,40 @@ TypeCast
|
|
|
2550
2334
|
= "::" type:("timestamptz"i / "timestamp"i / "boolean"i / "numeric"i / "bigint"i / "text"i / "date"i / "int"i)
|
|
2551
2335
|
{ return type.toLowerCase(); }
|
|
2552
2336
|
|
|
2337
|
+
QuotedString
|
|
2338
|
+
= '"' chars:DoubleQuotedChar* '"' { return chars.join(''); }
|
|
2339
|
+
/ "'" chars:SingleQuotedChar* "'" { return chars.join(''); }
|
|
2340
|
+
|
|
2341
|
+
DoubleQuotedChar
|
|
2342
|
+
= '\\\\"' { return '"'; }
|
|
2343
|
+
/ '\\\\\\\\' { return '\\\\'; }
|
|
2344
|
+
/ [^"\\\\]
|
|
2345
|
+
|
|
2346
|
+
SingleQuotedChar
|
|
2347
|
+
= "\\\\'" { return "'"; }
|
|
2348
|
+
/ '\\\\\\\\' { return '\\\\'; }
|
|
2349
|
+
/ [^'\\\\]
|
|
2350
|
+
|
|
2553
2351
|
Value
|
|
2554
|
-
=
|
|
2352
|
+
= QuotedString
|
|
2353
|
+
/ chars:ValueChar+ { return chars.join(''); }
|
|
2555
2354
|
|
|
2556
2355
|
ValueChar
|
|
2557
2356
|
= "\\\\\\\\" { return '\\\\\\\\'; }
|
|
2558
2357
|
/ "\\\\," { return '\\\\,'; }
|
|
2559
2358
|
/ "\\\\|" { return '\\\\|'; }
|
|
2560
|
-
/ [
|
|
2359
|
+
/ [a-zA-Z0-9_\\-!#/@$%^&*+=<>?~.;'" ]
|
|
2561
2360
|
/ c:":" !":" { return c; }
|
|
2562
2361
|
|
|
2563
2362
|
ValueWithPipes
|
|
2564
|
-
=
|
|
2363
|
+
= QuotedString
|
|
2364
|
+
/ chars:ValueWithPipesChar+ { return chars.join(''); }
|
|
2565
2365
|
|
|
2566
2366
|
ValueWithPipesChar
|
|
2567
2367
|
= "\\\\\\\\" { return '\\\\\\\\'; }
|
|
2568
2368
|
/ "\\\\," { return '\\\\,'; }
|
|
2569
2369
|
/ "\\\\|" { return '\\\\|'; }
|
|
2570
|
-
/ [
|
|
2370
|
+
/ [a-zA-Z0-9_\\-!#/@$%^&*+=<>?~.;'" |]
|
|
2571
2371
|
/ c:":" !":" { return c; }
|
|
2572
2372
|
`;
|
|
2573
2373
|
var fullGrammar = entryGrammar + oldGrammar + newGrammar;
|
|
@@ -2577,278 +2377,654 @@ var filterPsqlParser = peg.generate(fullGrammar, {
|
|
|
2577
2377
|
});
|
|
2578
2378
|
var filterPsqlParser_default = filterPsqlParser;
|
|
2579
2379
|
|
|
2580
|
-
// src/restura/sql/
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
this.setupTriggerListeners = this.listenForDbTriggers();
|
|
2597
|
-
}
|
|
2598
|
-
this.scratchDbName = `${psqlConnectionPool.poolConfig.database}_scratch${scratchDatabaseSuffix ? `_${scratchDatabaseSuffix}` : ""}`;
|
|
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();
|
|
2599
2396
|
}
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
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);
|
|
2414
|
+
}
|
|
2415
|
+
throw new RsError("DATABASE_ERROR", `${error.message}`);
|
|
2609
2416
|
}
|
|
2610
2417
|
}
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
TIMETZ: 1266
|
|
2623
|
-
};
|
|
2624
|
-
types.setTypeParser(PG_TYPE_OID.BIGINT, (val) => val === null ? null : Number(val));
|
|
2625
|
-
types.setTypeParser(PG_TYPE_OID.DATE, (val) => val);
|
|
2626
|
-
types.setTypeParser(PG_TYPE_OID.TIME, (val) => val);
|
|
2627
|
-
types.setTypeParser(PG_TYPE_OID.TIMETZ, (val) => val);
|
|
2628
|
-
types.setTypeParser(PG_TYPE_OID.TIMESTAMP, (val) => val === null ? null : new Date(val).toISOString());
|
|
2629
|
-
types.setTypeParser(PG_TYPE_OID.TIMESTAMPTZ, (val) => val === null ? null : new Date(val).toISOString());
|
|
2630
|
-
}
|
|
2631
|
-
async reconnectTriggerClient() {
|
|
2632
|
-
if (this.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) {
|
|
2633
|
-
logger.error("Max reconnection attempts reached for trigger client. Stopping reconnection attempts.");
|
|
2634
|
-
return;
|
|
2635
|
-
}
|
|
2636
|
-
if (this.triggerClient) {
|
|
2637
|
-
try {
|
|
2638
|
-
await this.triggerClient.end();
|
|
2639
|
-
} catch (error) {
|
|
2640
|
-
logger.error(`Error closing trigger client: ${error}`);
|
|
2418
|
+
async queryOneSchema(query, params, requesterDetails, zodSchema) {
|
|
2419
|
+
const result = await this.queryOne(query, params, requesterDetails);
|
|
2420
|
+
try {
|
|
2421
|
+
return zodSchema.parse(result);
|
|
2422
|
+
} catch (error) {
|
|
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);
|
|
2641
2429
|
}
|
|
2430
|
+
throw new RsError("DATABASE_ERROR", `Invalid data returned from database`);
|
|
2642
2431
|
}
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2432
|
+
}
|
|
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();
|
|
2649
2439
|
try {
|
|
2650
|
-
await this.
|
|
2651
|
-
this.
|
|
2440
|
+
const response = await this.query(queryMetadata + formattedQuery, options);
|
|
2441
|
+
this.logSqlStatement(formattedQuery, options, meta, startTime);
|
|
2442
|
+
return response.rows;
|
|
2652
2443
|
} catch (error) {
|
|
2653
|
-
|
|
2654
|
-
if (
|
|
2655
|
-
|
|
2444
|
+
this.logSqlStatement(formattedQuery, options, meta, startTime);
|
|
2445
|
+
if (error?.routine === "_bt_check_unique") {
|
|
2446
|
+
throw new RsError("DUPLICATE", error.message);
|
|
2656
2447
|
}
|
|
2448
|
+
throw new RsError("DATABASE_ERROR", `${error.message}`);
|
|
2657
2449
|
}
|
|
2658
2450
|
}
|
|
2659
|
-
async
|
|
2660
|
-
|
|
2661
|
-
user: this.psqlConnectionPool.poolConfig.user,
|
|
2662
|
-
host: this.psqlConnectionPool.poolConfig.host,
|
|
2663
|
-
database: this.psqlConnectionPool.poolConfig.database,
|
|
2664
|
-
password: this.psqlConnectionPool.poolConfig.password,
|
|
2665
|
-
port: this.psqlConnectionPool.poolConfig.port,
|
|
2666
|
-
connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
|
|
2667
|
-
});
|
|
2451
|
+
async runQuerySchema(query, params, requesterDetails, zodSchema) {
|
|
2452
|
+
const result = await this.runQuery(query, params, requesterDetails);
|
|
2668
2453
|
try {
|
|
2669
|
-
|
|
2670
|
-
const promises = [];
|
|
2671
|
-
promises.push(this.triggerClient.query("LISTEN insert"));
|
|
2672
|
-
promises.push(this.triggerClient.query("LISTEN update"));
|
|
2673
|
-
promises.push(this.triggerClient.query("LISTEN delete"));
|
|
2674
|
-
await Promise.all(promises);
|
|
2675
|
-
this.triggerClient.on("error", async (error) => {
|
|
2676
|
-
logger.error(`Trigger client error: ${error}`);
|
|
2677
|
-
await this.reconnectTriggerClient();
|
|
2678
|
-
});
|
|
2679
|
-
this.triggerClient.on("notification", async (msg) => {
|
|
2680
|
-
if (msg.channel === "insert" || msg.channel === "update" || msg.channel === "delete") {
|
|
2681
|
-
const payload = ObjectUtils3.safeParse(msg.payload);
|
|
2682
|
-
await this.handleTrigger(payload, msg.channel.toUpperCase());
|
|
2683
|
-
}
|
|
2684
|
-
});
|
|
2685
|
-
logger.info("Successfully connected to database triggers");
|
|
2454
|
+
return z4.array(zodSchema).parse(result);
|
|
2686
2455
|
} catch (error) {
|
|
2687
|
-
|
|
2688
|
-
|
|
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`);
|
|
2689
2464
|
}
|
|
2690
2465
|
}
|
|
2691
|
-
|
|
2692
|
-
if (
|
|
2693
|
-
|
|
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
|
+
});
|
|
2491
|
+
}
|
|
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}`);
|
|
2506
|
+
}
|
|
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(/^\//, "");
|
|
2694
2512
|
}
|
|
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
|
+
});
|
|
2695
2526
|
}
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
return sqlFullStatement;
|
|
2527
|
+
pool;
|
|
2528
|
+
async query(query, values) {
|
|
2529
|
+
return this.pool.query(query, values);
|
|
2700
2530
|
}
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
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);
|
|
2751
2809
|
}
|
|
2752
|
-
sql += "\n);";
|
|
2753
|
-
sqlStatements.push(sql);
|
|
2754
2810
|
}
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
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}"
|
|
2761
2820
|
FOREIGN KEY ("${foreignKey.column}") REFERENCES "${foreignKey.refTable}" ("${foreignKey.refColumn}")`;
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
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})`
|
|
2789
2968
|
);
|
|
2790
|
-
|
|
2791
|
-
|
|
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
|
+
}
|
|
2792
2979
|
}
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2980
|
+
}
|
|
2981
|
+
async listenForDbTriggers() {
|
|
2982
|
+
this.triggerClient = new Client2({
|
|
2796
2983
|
user: this.psqlConnectionPool.poolConfig.user,
|
|
2797
|
-
|
|
2984
|
+
host: this.psqlConnectionPool.poolConfig.host,
|
|
2985
|
+
database: this.psqlConnectionPool.poolConfig.database,
|
|
2798
2986
|
password: this.psqlConnectionPool.poolConfig.password,
|
|
2799
|
-
|
|
2800
|
-
idleTimeoutMillis: this.psqlConnectionPool.poolConfig.idleTimeoutMillis,
|
|
2987
|
+
port: this.psqlConnectionPool.poolConfig.port,
|
|
2801
2988
|
connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
|
|
2802
2989
|
});
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
);
|
|
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);
|
|
2824
3016
|
}
|
|
2825
|
-
|
|
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);
|
|
2826
3025
|
}
|
|
2827
3026
|
async diffDatabaseToSchema(schema) {
|
|
2828
|
-
|
|
2829
|
-
await this.createDatabaseFromSchema(schema, scratchPool);
|
|
2830
|
-
const originalClient = new Client({
|
|
2831
|
-
database: this.psqlConnectionPool.poolConfig.database,
|
|
2832
|
-
user: this.psqlConnectionPool.poolConfig.user,
|
|
2833
|
-
password: this.psqlConnectionPool.poolConfig.password,
|
|
2834
|
-
host: this.psqlConnectionPool.poolConfig.host,
|
|
2835
|
-
port: this.psqlConnectionPool.poolConfig.port
|
|
2836
|
-
});
|
|
2837
|
-
const scratchClient = new Client({
|
|
2838
|
-
database: this.scratchDbName,
|
|
2839
|
-
user: this.psqlConnectionPool.poolConfig.user,
|
|
2840
|
-
password: this.psqlConnectionPool.poolConfig.password,
|
|
2841
|
-
host: this.psqlConnectionPool.poolConfig.host,
|
|
2842
|
-
port: this.psqlConnectionPool.poolConfig.port
|
|
2843
|
-
});
|
|
2844
|
-
const promises = [originalClient.connect(), scratchClient.connect()];
|
|
2845
|
-
await Promise.all(promises);
|
|
2846
|
-
const infoPromises = [pgInfo({ client: originalClient }), pgInfo({ client: scratchClient })];
|
|
2847
|
-
const [info1, info2] = await Promise.all(infoPromises);
|
|
2848
|
-
const diff = getDiff(info1, info2);
|
|
2849
|
-
const endPromises = [originalClient.end(), scratchClient.end()];
|
|
2850
|
-
await Promise.all(endPromises);
|
|
2851
|
-
return diff.join("\n");
|
|
3027
|
+
return diffDatabaseToSchema(schema, this.psqlConnectionPool, this.scratchDbName);
|
|
2852
3028
|
}
|
|
2853
3029
|
createNestedSelect(req, schema, item, routeData, sqlParams) {
|
|
2854
3030
|
if (!item.subquery) return "";
|
|
@@ -3112,316 +3288,98 @@ DELETE FROM "${routeData.table}" ${joinStatement} ${whereClause}`;
|
|
|
3112
3288
|
});
|
|
3113
3289
|
return joinStatements;
|
|
3114
3290
|
}
|
|
3115
|
-
generateGroupBy(routeData) {
|
|
3116
|
-
let groupBy = "";
|
|
3117
|
-
if (routeData.groupBy) {
|
|
3118
|
-
groupBy = `GROUP BY ${escapeColumnName(routeData.groupBy.tableName)}.${escapeColumnName(routeData.groupBy.columnName)}
|
|
3119
|
-
`;
|
|
3120
|
-
}
|
|
3121
|
-
return groupBy;
|
|
3122
|
-
}
|
|
3123
|
-
generateOrderBy(req, routeData) {
|
|
3124
|
-
let orderBy = "";
|
|
3125
|
-
const orderOptions = {
|
|
3126
|
-
ASC: "ASC",
|
|
3127
|
-
DESC: "DESC"
|
|
3128
|
-
};
|
|
3129
|
-
const data = req.data;
|
|
3130
|
-
if (routeData.type === "PAGED" && "sortBy" in data) {
|
|
3131
|
-
const sortOrder = orderOptions[data.sortOrder] || "ASC";
|
|
3132
|
-
orderBy = `ORDER BY ${escapeColumnName(data.sortBy)} ${sortOrder}
|
|
3133
|
-
`;
|
|
3134
|
-
} else if (routeData.orderBy) {
|
|
3135
|
-
const sortOrder = orderOptions[routeData.orderBy.order] || "ASC";
|
|
3136
|
-
orderBy = `ORDER BY ${escapeColumnName(routeData.orderBy.tableName)}.${escapeColumnName(routeData.orderBy.columnName)} ${sortOrder}
|
|
3137
|
-
`;
|
|
3138
|
-
}
|
|
3139
|
-
return orderBy;
|
|
3140
|
-
}
|
|
3141
|
-
generateWhereClause(req, where, routeData, sqlParams) {
|
|
3142
|
-
let whereClause = "";
|
|
3143
|
-
where.forEach((item, index) => {
|
|
3144
|
-
if (index === 0) whereClause = "WHERE ";
|
|
3145
|
-
if (item.custom) {
|
|
3146
|
-
const customReplaced = this.replaceParamKeywords(item.custom, routeData, req, sqlParams);
|
|
3147
|
-
whereClause += ` ${item.conjunction || ""} ${customReplaced}
|
|
3148
|
-
`;
|
|
3149
|
-
return;
|
|
3150
|
-
}
|
|
3151
|
-
if (item.operator === void 0 || item.value === void 0 || item.columnName === void 0 || item.tableName === void 0)
|
|
3152
|
-
throw new RsError(
|
|
3153
|
-
"SCHEMA_ERROR",
|
|
3154
|
-
`Invalid where clause in route ${routeData.name}, missing required fields if not custom`
|
|
3155
|
-
);
|
|
3156
|
-
let operator = item.operator;
|
|
3157
|
-
let value = item.value;
|
|
3158
|
-
if (operator === "LIKE") {
|
|
3159
|
-
value = `'%' || ${value} || '%'`;
|
|
3160
|
-
} else if (operator === "NOT LIKE") {
|
|
3161
|
-
value = `'%' || ${value} || '%'`;
|
|
3162
|
-
} else if (operator === "STARTS WITH") {
|
|
3163
|
-
operator = "LIKE";
|
|
3164
|
-
value = `${value} || '%'`;
|
|
3165
|
-
} else if (operator === "ENDS WITH") {
|
|
3166
|
-
operator = "LIKE";
|
|
3167
|
-
value = `'%' || ${value}`;
|
|
3168
|
-
}
|
|
3169
|
-
const replacedValue = this.replaceParamKeywords(value, routeData, req, sqlParams);
|
|
3170
|
-
whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator.replace("LIKE", "ILIKE")} ${["IN", "NOT IN"].includes(operator) ? `(${replacedValue})` : replacedValue}
|
|
3171
|
-
`;
|
|
3172
|
-
});
|
|
3173
|
-
const data = req.data;
|
|
3174
|
-
if (routeData.type === "PAGED" && !!data?.filter) {
|
|
3175
|
-
let statement = data.filter.replace(/\$[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
|
|
3176
|
-
const requestParam = routeData.request.find((item) => {
|
|
3177
|
-
return item.name === value.replace("$", "");
|
|
3178
|
-
});
|
|
3179
|
-
if (!requestParam)
|
|
3180
|
-
throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
|
|
3181
|
-
return data[requestParam.name]?.toString() || "";
|
|
3182
|
-
});
|
|
3183
|
-
statement = statement.replace(/#[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
|
|
3184
|
-
const requestParam = routeData.request.find((item) => {
|
|
3185
|
-
return item.name === value.replace("#", "");
|
|
3186
|
-
});
|
|
3187
|
-
if (!requestParam)
|
|
3188
|
-
throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
|
|
3189
|
-
return data[requestParam.name]?.toString() || "";
|
|
3190
|
-
});
|
|
3191
|
-
const parseResult = filterPsqlParser_default.parse(statement);
|
|
3192
|
-
if (parseResult.usedOldSyntax) {
|
|
3193
|
-
logger.warn(
|
|
3194
|
-
`Deprecated filter syntax detected in route "${routeData.name}" (${routeData.path}). Please migrate to the new filter syntax.`
|
|
3195
|
-
);
|
|
3196
|
-
}
|
|
3197
|
-
statement = parseResult.sql;
|
|
3198
|
-
if (whereClause.startsWith("WHERE")) {
|
|
3199
|
-
whereClause += ` AND (${statement})
|
|
3200
|
-
`;
|
|
3201
|
-
} else {
|
|
3202
|
-
whereClause += `WHERE ${statement}
|
|
3203
|
-
`;
|
|
3204
|
-
}
|
|
3205
|
-
}
|
|
3206
|
-
return whereClause;
|
|
3207
|
-
}
|
|
3208
|
-
createUpdateTrigger(tableName, notify) {
|
|
3209
|
-
if (!notify) return "";
|
|
3210
|
-
if (notify === "ALL") {
|
|
3211
|
-
return `
|
|
3212
|
-
CREATE OR REPLACE FUNCTION notify_${tableName}_update()
|
|
3213
|
-
RETURNS TRIGGER AS $$
|
|
3214
|
-
DECLARE
|
|
3215
|
-
query_metadata JSON;
|
|
3216
|
-
BEGIN
|
|
3217
|
-
SELECT INTO query_metadata
|
|
3218
|
-
(regexp_match(
|
|
3219
|
-
current_query(),
|
|
3220
|
-
'^--QUERY_METADATA\\(({.*})', 'n'
|
|
3221
|
-
))[1]::json;
|
|
3222
|
-
|
|
3223
|
-
PERFORM pg_notify(
|
|
3224
|
-
'update',
|
|
3225
|
-
json_build_object(
|
|
3226
|
-
'table', '${tableName}',
|
|
3227
|
-
'queryMetadata', query_metadata,
|
|
3228
|
-
'changedId', NEW.id,
|
|
3229
|
-
'record', NEW,
|
|
3230
|
-
'previousRecord', OLD
|
|
3231
|
-
)::text
|
|
3232
|
-
);
|
|
3233
|
-
RETURN NEW;
|
|
3234
|
-
END;
|
|
3235
|
-
$$ LANGUAGE plpgsql;
|
|
3236
|
-
|
|
3237
|
-
CREATE OR REPLACE TRIGGER ${tableName}_update
|
|
3238
|
-
AFTER UPDATE ON "${tableName}"
|
|
3239
|
-
FOR EACH ROW
|
|
3240
|
-
EXECUTE FUNCTION notify_${tableName}_update();
|
|
3241
|
-
`;
|
|
3242
|
-
}
|
|
3243
|
-
const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
|
|
3244
|
-
const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
|
|
3245
|
-
return `
|
|
3246
|
-
CREATE OR REPLACE FUNCTION notify_${tableName}_update()
|
|
3247
|
-
RETURNS TRIGGER AS $$
|
|
3248
|
-
DECLARE
|
|
3249
|
-
query_metadata JSON;
|
|
3250
|
-
BEGIN
|
|
3251
|
-
SELECT INTO query_metadata
|
|
3252
|
-
(regexp_match(
|
|
3253
|
-
current_query(),
|
|
3254
|
-
'^--QUERY_METADATA\\(({.*})', 'n'
|
|
3255
|
-
))[1]::json;
|
|
3256
|
-
|
|
3257
|
-
PERFORM pg_notify(
|
|
3258
|
-
'update',
|
|
3259
|
-
json_build_object(
|
|
3260
|
-
'table', '${tableName}',
|
|
3261
|
-
'queryMetadata', query_metadata,
|
|
3262
|
-
'changedId', NEW.id,
|
|
3263
|
-
'record', json_build_object(
|
|
3264
|
-
${notifyColumnNewBuildString}
|
|
3265
|
-
),
|
|
3266
|
-
'previousRecord', json_build_object(
|
|
3267
|
-
${notifyColumnOldBuildString}
|
|
3268
|
-
)
|
|
3269
|
-
)::text
|
|
3270
|
-
);
|
|
3271
|
-
RETURN NEW;
|
|
3272
|
-
END;
|
|
3273
|
-
$$ LANGUAGE plpgsql;
|
|
3274
|
-
|
|
3275
|
-
CREATE OR REPLACE TRIGGER ${tableName}_update
|
|
3276
|
-
AFTER UPDATE ON "${tableName}"
|
|
3277
|
-
FOR EACH ROW
|
|
3278
|
-
EXECUTE FUNCTION notify_${tableName}_update();
|
|
3279
|
-
`;
|
|
3280
|
-
}
|
|
3281
|
-
createDeleteTrigger(tableName, notify) {
|
|
3282
|
-
if (!notify) return "";
|
|
3283
|
-
if (notify === "ALL") {
|
|
3284
|
-
return `
|
|
3285
|
-
CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
|
|
3286
|
-
RETURNS TRIGGER AS $$
|
|
3287
|
-
DECLARE
|
|
3288
|
-
query_metadata JSON;
|
|
3289
|
-
BEGIN
|
|
3290
|
-
SELECT INTO query_metadata
|
|
3291
|
-
(regexp_match(
|
|
3292
|
-
current_query(),
|
|
3293
|
-
'^--QUERY_METADATA\\(({.*})', 'n'
|
|
3294
|
-
))[1]::json;
|
|
3295
|
-
|
|
3296
|
-
PERFORM pg_notify(
|
|
3297
|
-
'delete',
|
|
3298
|
-
json_build_object(
|
|
3299
|
-
'table', '${tableName}',
|
|
3300
|
-
'queryMetadata', query_metadata,
|
|
3301
|
-
'deletedId', OLD.id,
|
|
3302
|
-
'previousRecord', OLD
|
|
3303
|
-
)::text
|
|
3304
|
-
);
|
|
3305
|
-
RETURN NEW;
|
|
3306
|
-
END;
|
|
3307
|
-
$$ LANGUAGE plpgsql;
|
|
3308
|
-
|
|
3309
|
-
CREATE OR REPLACE TRIGGER "${tableName}_delete"
|
|
3310
|
-
AFTER DELETE ON "${tableName}"
|
|
3311
|
-
FOR EACH ROW
|
|
3312
|
-
EXECUTE FUNCTION notify_${tableName}_delete();
|
|
3313
|
-
`;
|
|
3314
|
-
}
|
|
3315
|
-
const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
|
|
3316
|
-
return `
|
|
3317
|
-
CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
|
|
3318
|
-
RETURNS TRIGGER AS $$
|
|
3319
|
-
DECLARE
|
|
3320
|
-
query_metadata JSON;
|
|
3321
|
-
BEGIN
|
|
3322
|
-
SELECT INTO query_metadata
|
|
3323
|
-
(regexp_match(
|
|
3324
|
-
current_query(),
|
|
3325
|
-
'^--QUERY_METADATA\\(({.*})', 'n'
|
|
3326
|
-
))[1]::json;
|
|
3327
|
-
|
|
3328
|
-
PERFORM pg_notify(
|
|
3329
|
-
'delete',
|
|
3330
|
-
json_build_object(
|
|
3331
|
-
'table', '${tableName}',
|
|
3332
|
-
'queryMetadata', query_metadata,
|
|
3333
|
-
'deletedId', OLD.id,
|
|
3334
|
-
'previousRecord', json_build_object(
|
|
3335
|
-
${notifyColumnOldBuildString}
|
|
3336
|
-
)
|
|
3337
|
-
)::text
|
|
3338
|
-
);
|
|
3339
|
-
RETURN NEW;
|
|
3340
|
-
END;
|
|
3341
|
-
$$ LANGUAGE plpgsql;
|
|
3342
|
-
|
|
3343
|
-
CREATE OR REPLACE TRIGGER "${tableName}_delete"
|
|
3344
|
-
AFTER DELETE ON "${tableName}"
|
|
3345
|
-
FOR EACH ROW
|
|
3346
|
-
EXECUTE FUNCTION notify_${tableName}_delete();
|
|
3347
|
-
`;
|
|
3348
|
-
}
|
|
3349
|
-
createInsertTriggers(tableName, notify) {
|
|
3350
|
-
if (!notify) return "";
|
|
3351
|
-
if (notify === "ALL") {
|
|
3352
|
-
return `
|
|
3353
|
-
CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
|
|
3354
|
-
RETURNS TRIGGER AS $$
|
|
3355
|
-
DECLARE
|
|
3356
|
-
query_metadata JSON;
|
|
3357
|
-
BEGIN
|
|
3358
|
-
SELECT INTO query_metadata
|
|
3359
|
-
(regexp_match(
|
|
3360
|
-
current_query(),
|
|
3361
|
-
'^--QUERY_METADATA\\(({.*})', 'n'
|
|
3362
|
-
))[1]::json;
|
|
3363
|
-
|
|
3364
|
-
PERFORM pg_notify(
|
|
3365
|
-
'insert',
|
|
3366
|
-
json_build_object(
|
|
3367
|
-
'table', '${tableName}',
|
|
3368
|
-
'queryMetadata', query_metadata,
|
|
3369
|
-
'insertedId', NEW.id,
|
|
3370
|
-
'record', NEW
|
|
3371
|
-
)::text
|
|
3372
|
-
);
|
|
3373
|
-
|
|
3374
|
-
RETURN NEW;
|
|
3375
|
-
END;
|
|
3376
|
-
$$ LANGUAGE plpgsql;
|
|
3377
|
-
|
|
3378
|
-
CREATE OR REPLACE TRIGGER "${tableName}_insert"
|
|
3379
|
-
AFTER INSERT ON "${tableName}"
|
|
3380
|
-
FOR EACH ROW
|
|
3381
|
-
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)}
|
|
3382
3295
|
`;
|
|
3383
3296
|
}
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
'table', '${tableName}',
|
|
3401
|
-
'queryMetadata', query_metadata,
|
|
3402
|
-
'insertedId', NEW.id,
|
|
3403
|
-
'record', json_build_object(
|
|
3404
|
-
${notifyColumnNewBuildString}
|
|
3405
|
-
)
|
|
3406
|
-
)::text
|
|
3407
|
-
);
|
|
3408
|
-
|
|
3409
|
-
RETURN NEW;
|
|
3410
|
-
END;
|
|
3411
|
-
$$ LANGUAGE plpgsql;
|
|
3412
|
-
|
|
3413
|
-
CREATE OR REPLACE TRIGGER "${tableName}_insert"
|
|
3414
|
-
AFTER INSERT ON "${tableName}"
|
|
3415
|
-
FOR EACH ROW
|
|
3416
|
-
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}
|
|
3417
3313
|
`;
|
|
3314
|
+
}
|
|
3315
|
+
return orderBy;
|
|
3418
3316
|
}
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
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;
|
|
3425
3383
|
}
|
|
3426
3384
|
};
|
|
3427
3385
|
|
|
@@ -3498,8 +3456,9 @@ var ResturaEngine = class {
|
|
|
3498
3456
|
* @param app - The Express application instance to initialize with Restura.
|
|
3499
3457
|
* @returns A promise that resolves when the initialization is complete.
|
|
3500
3458
|
*/
|
|
3501
|
-
async init(app, authenticationHandler, psqlConnectionPool) {
|
|
3502
|
-
|
|
3459
|
+
async init(app, authenticationHandler, psqlConnectionPool, options) {
|
|
3460
|
+
if (options?.logger) setLogger(options.logger);
|
|
3461
|
+
this.resturaConfig = await config.validate("restura", resturaConfigSchema);
|
|
3503
3462
|
this.multerCommonUpload = getMulterUpload(this.resturaConfig.fileTempCachePath);
|
|
3504
3463
|
new TempCache(this.resturaConfig.fileTempCachePath);
|
|
3505
3464
|
this.psqlConnectionPool = psqlConnectionPool;
|
|
@@ -3853,14 +3812,556 @@ __decorateClass([
|
|
|
3853
3812
|
], ResturaEngine.prototype, "runCustomRouteLogic", 1);
|
|
3854
3813
|
var restura = new ResturaEngine();
|
|
3855
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
|
+
|
|
3856
4357
|
// src/restura/sql/PsqlTransaction.ts
|
|
3857
|
-
import
|
|
3858
|
-
var { Client:
|
|
4358
|
+
import pg4 from "pg";
|
|
4359
|
+
var { Client: Client3 } = pg4;
|
|
3859
4360
|
var PsqlTransaction = class extends PsqlConnection {
|
|
3860
4361
|
constructor(clientConfig, instanceId) {
|
|
3861
4362
|
super(instanceId);
|
|
3862
4363
|
this.clientConfig = clientConfig;
|
|
3863
|
-
this.client = new
|
|
4364
|
+
this.client = new Client3(clientConfig);
|
|
3864
4365
|
this.connectPromise = this.client.connect();
|
|
3865
4366
|
this.beginTransactionPromise = this.beginTransaction();
|
|
3866
4367
|
}
|
|
@@ -3900,10 +4401,18 @@ export {
|
|
|
3900
4401
|
RsError,
|
|
3901
4402
|
SQL,
|
|
3902
4403
|
apiGenerator,
|
|
4404
|
+
createDeleteTriggerSql,
|
|
4405
|
+
createInsertTriggerSql,
|
|
4406
|
+
createUpdateTriggerSql,
|
|
4407
|
+
diffDatabaseToSchema,
|
|
4408
|
+
diffSchemaToDatabase,
|
|
3903
4409
|
escapeColumnName,
|
|
3904
4410
|
eventManager_default as eventManager,
|
|
3905
4411
|
filterPsqlParser_default as filterPsqlParser,
|
|
4412
|
+
generateDatabaseSchemaFromSchema,
|
|
4413
|
+
getNewPublicSchemaAndScratchPool,
|
|
3906
4414
|
insertObjectQuery,
|
|
4415
|
+
introspectDatabase,
|
|
3907
4416
|
isSchemaValid,
|
|
3908
4417
|
isValueNumber,
|
|
3909
4418
|
logger,
|
|
@@ -3912,6 +4421,9 @@ export {
|
|
|
3912
4421
|
restura,
|
|
3913
4422
|
resturaGlobalTypesGenerator,
|
|
3914
4423
|
resturaSchema,
|
|
4424
|
+
schemaToPsqlType,
|
|
4425
|
+
setLogger,
|
|
4426
|
+
systemUser,
|
|
3915
4427
|
toSqlLiteral,
|
|
3916
4428
|
updateObjectQuery
|
|
3917
4429
|
};
|