@restura/core 0.1.0-alpha.9 → 0.1.2
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.mts +1428 -285
- package/dist/index.d.ts +1428 -285
- package/dist/index.js +1372 -458
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1356 -447
- package/dist/index.mjs.map +1 -1
- package/package.json +25 -16
package/dist/index.mjs
CHANGED
|
@@ -44,18 +44,11 @@ import { config } from "@restura/internal";
|
|
|
44
44
|
import winston from "winston";
|
|
45
45
|
import { format } from "logform";
|
|
46
46
|
|
|
47
|
-
// src/
|
|
47
|
+
// src/logger/loggerConfigSchema.ts
|
|
48
48
|
import { z } from "zod";
|
|
49
49
|
var loggerConfigSchema = z.object({
|
|
50
50
|
level: z.enum(["info", "warn", "error", "debug", "silly"]).default("info")
|
|
51
51
|
});
|
|
52
|
-
var resturaConfigSchema = z.object({
|
|
53
|
-
authToken: z.string().min(1, "Missing Restura Auth Token"),
|
|
54
|
-
sendErrorStackTrace: z.boolean().default(false),
|
|
55
|
-
schemaFilePath: z.string().default(process.cwd() + "/restura.schema.json"),
|
|
56
|
-
customApiFolderPath: z.string().default(process.cwd() + "/dist/api"),
|
|
57
|
-
generatedTypesPath: z.string().default(process.cwd() + "/src/@types")
|
|
58
|
-
});
|
|
59
52
|
|
|
60
53
|
// src/logger/logger.ts
|
|
61
54
|
var loggerConfig = config.validate("logger", loggerConfigSchema);
|
|
@@ -92,7 +85,198 @@ var logger = winston.createLogger({
|
|
|
92
85
|
]
|
|
93
86
|
});
|
|
94
87
|
|
|
95
|
-
// src/restura/
|
|
88
|
+
// src/restura/eventManager.ts
|
|
89
|
+
import Bluebird from "bluebird";
|
|
90
|
+
var EventManager = class {
|
|
91
|
+
constructor() {
|
|
92
|
+
this.actionHandlers = {
|
|
93
|
+
DATABASE_ROW_DELETE: [],
|
|
94
|
+
DATABASE_ROW_INSERT: [],
|
|
95
|
+
DATABASE_COLUMN_UPDATE: []
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
addRowInsertHandler(onInsert, filter) {
|
|
99
|
+
this.actionHandlers.DATABASE_ROW_INSERT.push({
|
|
100
|
+
callback: onInsert,
|
|
101
|
+
filter
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
addColumnChangeHandler(onUpdate, filter) {
|
|
105
|
+
this.actionHandlers.DATABASE_COLUMN_UPDATE.push({
|
|
106
|
+
callback: onUpdate,
|
|
107
|
+
filter
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
addRowDeleteHandler(onDelete, filter) {
|
|
111
|
+
this.actionHandlers.DATABASE_ROW_DELETE.push({
|
|
112
|
+
callback: onDelete,
|
|
113
|
+
filter
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async fireActionFromDbTrigger(sqlMutationData, result) {
|
|
117
|
+
if (sqlMutationData.mutationType === "INSERT") {
|
|
118
|
+
await this.fireInsertActions(sqlMutationData, result);
|
|
119
|
+
} else if (sqlMutationData.mutationType === "UPDATE") {
|
|
120
|
+
await this.fireUpdateActions(sqlMutationData, result);
|
|
121
|
+
} else if (sqlMutationData.mutationType === "DELETE") {
|
|
122
|
+
await this.fireDeleteActions(sqlMutationData, result);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async fireInsertActions(data, triggerResult) {
|
|
126
|
+
await Bluebird.map(
|
|
127
|
+
this.actionHandlers.DATABASE_ROW_INSERT,
|
|
128
|
+
({ callback, filter }) => {
|
|
129
|
+
if (!this.hasHandlersForEventType("DATABASE_ROW_INSERT", filter, triggerResult)) return;
|
|
130
|
+
const insertData = {
|
|
131
|
+
tableName: triggerResult.table,
|
|
132
|
+
insertedId: triggerResult.insertedId || 0,
|
|
133
|
+
insertObject: triggerResult.record,
|
|
134
|
+
queryMetadata: data.queryMetadata
|
|
135
|
+
};
|
|
136
|
+
callback(insertData, data.queryMetadata);
|
|
137
|
+
},
|
|
138
|
+
{ concurrency: 10 }
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
async fireDeleteActions(data, triggerResult) {
|
|
142
|
+
await Bluebird.map(
|
|
143
|
+
this.actionHandlers.DATABASE_ROW_DELETE,
|
|
144
|
+
({ callback, filter }) => {
|
|
145
|
+
if (!this.hasHandlersForEventType("DATABASE_ROW_DELETE", filter, triggerResult)) return;
|
|
146
|
+
const deleteData = {
|
|
147
|
+
tableName: triggerResult.table,
|
|
148
|
+
deletedId: triggerResult.deletedId || 0,
|
|
149
|
+
deletedRow: triggerResult.previousRecord,
|
|
150
|
+
queryMetadata: data.queryMetadata
|
|
151
|
+
};
|
|
152
|
+
callback(deleteData, data.queryMetadata);
|
|
153
|
+
},
|
|
154
|
+
{ concurrency: 10 }
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
async fireUpdateActions(data, triggerResult) {
|
|
158
|
+
await Bluebird.map(
|
|
159
|
+
this.actionHandlers.DATABASE_COLUMN_UPDATE,
|
|
160
|
+
({ callback, filter }) => {
|
|
161
|
+
if (!this.hasHandlersForEventType("DATABASE_COLUMN_UPDATE", filter, triggerResult)) return;
|
|
162
|
+
const columnChangeData = {
|
|
163
|
+
tableName: triggerResult.table,
|
|
164
|
+
changedId: triggerResult.changedId || 0,
|
|
165
|
+
newData: triggerResult.record,
|
|
166
|
+
oldData: triggerResult.previousRecord,
|
|
167
|
+
queryMetadata: data.queryMetadata
|
|
168
|
+
};
|
|
169
|
+
callback(columnChangeData, data.queryMetadata);
|
|
170
|
+
},
|
|
171
|
+
{ concurrency: 10 }
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
hasHandlersForEventType(eventType, filter, triggerResult) {
|
|
175
|
+
if (filter) {
|
|
176
|
+
switch (eventType) {
|
|
177
|
+
case "DATABASE_ROW_INSERT":
|
|
178
|
+
case "DATABASE_ROW_DELETE":
|
|
179
|
+
if (filter.tableName && filter.tableName !== triggerResult.table) return false;
|
|
180
|
+
break;
|
|
181
|
+
case "DATABASE_COLUMN_UPDATE":
|
|
182
|
+
const filterColumnChange = filter;
|
|
183
|
+
if (filterColumnChange.tableName !== triggerResult.table) return false;
|
|
184
|
+
if (filterColumnChange.columns.length === 1) {
|
|
185
|
+
const firstColumn = filterColumnChange.columns[0];
|
|
186
|
+
if (firstColumn === "*") return true;
|
|
187
|
+
}
|
|
188
|
+
if (!filterColumnChange.columns.some((item) => {
|
|
189
|
+
const updatedColumns = Object.keys(
|
|
190
|
+
changedValues(triggerResult.record, triggerResult.previousRecord)
|
|
191
|
+
);
|
|
192
|
+
return updatedColumns.includes(item);
|
|
193
|
+
}))
|
|
194
|
+
return false;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
var eventManager = new EventManager();
|
|
202
|
+
var eventManager_default = eventManager;
|
|
203
|
+
function changedValues(record, previousRecord) {
|
|
204
|
+
const changed = {};
|
|
205
|
+
for (const i in previousRecord) {
|
|
206
|
+
if (previousRecord[i] !== record[i]) {
|
|
207
|
+
if (typeof previousRecord[i] === "object" && typeof record[i] === "object") {
|
|
208
|
+
const nestedChanged = changedValues(record[i], previousRecord[i]);
|
|
209
|
+
if (Object.keys(nestedChanged).length > 0) {
|
|
210
|
+
changed[i] = record[i];
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
changed[i] = record[i];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return changed;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/restura/restura.ts
|
|
221
|
+
import { ObjectUtils as ObjectUtils5, StringUtils as StringUtils3 } from "@redskytech/core-utils";
|
|
222
|
+
import { config as config2 } from "@restura/internal";
|
|
223
|
+
|
|
224
|
+
// ../../node_modules/.pnpm/autobind-decorator@2.4.0/node_modules/autobind-decorator/lib/esm/index.js
|
|
225
|
+
function _typeof(obj) {
|
|
226
|
+
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
|
|
227
|
+
_typeof = function _typeof2(obj2) {
|
|
228
|
+
return typeof obj2;
|
|
229
|
+
};
|
|
230
|
+
} else {
|
|
231
|
+
_typeof = function _typeof2(obj2) {
|
|
232
|
+
return obj2 && typeof Symbol === "function" && obj2.constructor === Symbol && obj2 !== Symbol.prototype ? "symbol" : typeof obj2;
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return _typeof(obj);
|
|
236
|
+
}
|
|
237
|
+
function boundMethod(target, key, descriptor) {
|
|
238
|
+
var fn = descriptor.value;
|
|
239
|
+
if (typeof fn !== "function") {
|
|
240
|
+
throw new TypeError("@boundMethod decorator can only be applied to methods not: ".concat(_typeof(fn)));
|
|
241
|
+
}
|
|
242
|
+
var definingProperty = false;
|
|
243
|
+
return {
|
|
244
|
+
configurable: true,
|
|
245
|
+
get: function get() {
|
|
246
|
+
if (definingProperty || this === target.prototype || this.hasOwnProperty(key) || typeof fn !== "function") {
|
|
247
|
+
return fn;
|
|
248
|
+
}
|
|
249
|
+
var boundFn = fn.bind(this);
|
|
250
|
+
definingProperty = true;
|
|
251
|
+
Object.defineProperty(this, key, {
|
|
252
|
+
configurable: true,
|
|
253
|
+
get: function get2() {
|
|
254
|
+
return boundFn;
|
|
255
|
+
},
|
|
256
|
+
set: function set(value) {
|
|
257
|
+
fn = value;
|
|
258
|
+
delete this[key];
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
definingProperty = false;
|
|
262
|
+
return boundFn;
|
|
263
|
+
},
|
|
264
|
+
set: function set(value) {
|
|
265
|
+
fn = value;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/restura/restura.ts
|
|
271
|
+
import bodyParser from "body-parser";
|
|
272
|
+
import compression from "compression";
|
|
273
|
+
import cookieParser from "cookie-parser";
|
|
274
|
+
import * as express from "express";
|
|
275
|
+
import fs4 from "fs";
|
|
276
|
+
import path4 from "path";
|
|
277
|
+
import * as prettier3 from "prettier";
|
|
278
|
+
|
|
279
|
+
// src/restura/RsError.ts
|
|
96
280
|
var HtmlStatusCodes = /* @__PURE__ */ ((HtmlStatusCodes2) => {
|
|
97
281
|
HtmlStatusCodes2[HtmlStatusCodes2["BAD_REQUEST"] = 400] = "BAD_REQUEST";
|
|
98
282
|
HtmlStatusCodes2[HtmlStatusCodes2["UNAUTHORIZED"] = 401] = "UNAUTHORIZED";
|
|
@@ -116,7 +300,6 @@ var RsError = class _RsError {
|
|
|
116
300
|
static htmlStatus(code) {
|
|
117
301
|
return htmlStatusMap[code];
|
|
118
302
|
}
|
|
119
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
120
303
|
static isRsError(error) {
|
|
121
304
|
return error instanceof _RsError;
|
|
122
305
|
}
|
|
@@ -158,73 +341,171 @@ var htmlStatusMap = {
|
|
|
158
341
|
SCHEMA_ERROR: 500 /* SERVER_ERROR */
|
|
159
342
|
};
|
|
160
343
|
|
|
161
|
-
// src/restura/
|
|
162
|
-
import
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
344
|
+
// src/restura/compareSchema.ts
|
|
345
|
+
import cloneDeep from "lodash.clonedeep";
|
|
346
|
+
var CompareSchema = class {
|
|
347
|
+
async diffSchema(newSchema, latestSchema, psqlEngine) {
|
|
348
|
+
const endPoints = this.diffEndPoints(newSchema.endpoints[0].routes, latestSchema.endpoints[0].routes);
|
|
349
|
+
const globalParams = this.diffStringArray(newSchema.globalParams, latestSchema.globalParams);
|
|
350
|
+
const roles = this.diffStringArray(newSchema.roles, latestSchema.roles);
|
|
351
|
+
let commands = "";
|
|
352
|
+
if (JSON.stringify(newSchema.database) !== JSON.stringify(latestSchema.database))
|
|
353
|
+
commands = await psqlEngine.diffDatabaseToSchema(newSchema);
|
|
354
|
+
const hasCustomTypesChanged = JSON.stringify(newSchema.customTypes) !== JSON.stringify(latestSchema.customTypes);
|
|
355
|
+
const schemaPreview = {
|
|
356
|
+
endPoints,
|
|
357
|
+
globalParams,
|
|
358
|
+
roles,
|
|
359
|
+
commands,
|
|
360
|
+
customTypes: hasCustomTypesChanged
|
|
174
361
|
};
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
362
|
+
return schemaPreview;
|
|
363
|
+
}
|
|
364
|
+
diffStringArray(newArray, originalArray) {
|
|
365
|
+
const stringsDiff = [];
|
|
366
|
+
const originalClone = new Set(originalArray);
|
|
367
|
+
newArray.forEach((item) => {
|
|
368
|
+
const originalIndex = originalClone.has(item);
|
|
369
|
+
if (!originalIndex) {
|
|
370
|
+
stringsDiff.push({
|
|
371
|
+
name: item,
|
|
372
|
+
changeType: "NEW"
|
|
373
|
+
});
|
|
374
|
+
} else {
|
|
375
|
+
originalClone.delete(item);
|
|
189
376
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
377
|
+
});
|
|
378
|
+
originalClone.forEach((item) => {
|
|
379
|
+
stringsDiff.push({
|
|
380
|
+
name: item,
|
|
381
|
+
changeType: "DELETED"
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
return stringsDiff;
|
|
385
|
+
}
|
|
386
|
+
diffEndPoints(newEndPoints, originalEndpoints) {
|
|
387
|
+
const originalClone = cloneDeep(originalEndpoints);
|
|
388
|
+
const diffObj = [];
|
|
389
|
+
newEndPoints.forEach((endPoint) => {
|
|
390
|
+
const { path: path5, method } = endPoint;
|
|
391
|
+
const endPointIndex = originalClone.findIndex((original) => {
|
|
392
|
+
return original.path === endPoint.path && original.method === endPoint.method;
|
|
393
|
+
});
|
|
394
|
+
if (endPointIndex === -1) {
|
|
395
|
+
diffObj.push({
|
|
396
|
+
name: `${method} ${path5}`,
|
|
397
|
+
changeType: "NEW"
|
|
398
|
+
});
|
|
399
|
+
} else {
|
|
400
|
+
const original = originalClone.findIndex((original2) => {
|
|
401
|
+
return this.compareEndPoints(endPoint, original2);
|
|
402
|
+
});
|
|
403
|
+
if (original === -1) {
|
|
404
|
+
diffObj.push({
|
|
405
|
+
name: `${method} ${path5}`,
|
|
406
|
+
changeType: "MODIFIED"
|
|
407
|
+
});
|
|
200
408
|
}
|
|
409
|
+
originalClone.splice(endPointIndex, 1);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
originalClone.forEach((original) => {
|
|
413
|
+
const { path: path5, method } = original;
|
|
414
|
+
diffObj.push({
|
|
415
|
+
name: `${method} ${path5}`,
|
|
416
|
+
changeType: "DELETED"
|
|
201
417
|
});
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
418
|
+
});
|
|
419
|
+
return diffObj;
|
|
420
|
+
}
|
|
421
|
+
compareEndPoints(endPoint1, endPoint2) {
|
|
422
|
+
return JSON.stringify(endPoint1) === JSON.stringify(endPoint2);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
__decorateClass([
|
|
426
|
+
boundMethod
|
|
427
|
+
], CompareSchema.prototype, "diffSchema", 1);
|
|
428
|
+
__decorateClass([
|
|
429
|
+
boundMethod
|
|
430
|
+
], CompareSchema.prototype, "diffStringArray", 1);
|
|
431
|
+
__decorateClass([
|
|
432
|
+
boundMethod
|
|
433
|
+
], CompareSchema.prototype, "diffEndPoints", 1);
|
|
434
|
+
__decorateClass([
|
|
435
|
+
boundMethod
|
|
436
|
+
], CompareSchema.prototype, "compareEndPoints", 1);
|
|
437
|
+
var compareSchema = new CompareSchema();
|
|
438
|
+
var compareSchema_default = compareSchema;
|
|
439
|
+
|
|
440
|
+
// src/restura/customApiFactory.ts
|
|
441
|
+
import Bluebird2 from "bluebird";
|
|
442
|
+
import fs from "fs";
|
|
443
|
+
import path from "path";
|
|
444
|
+
import { FileUtils } from "@restura/internal";
|
|
445
|
+
var CustomApiFactory = class {
|
|
446
|
+
constructor() {
|
|
447
|
+
this.customApis = {};
|
|
448
|
+
}
|
|
449
|
+
async loadApiFiles(baseFolderPath) {
|
|
450
|
+
const apiVersions = ["v1"];
|
|
451
|
+
for (const apiVersion of apiVersions) {
|
|
452
|
+
const apiVersionFolderPath = path.join(baseFolderPath, apiVersion);
|
|
453
|
+
const directoryExists = await FileUtils.existDir(apiVersionFolderPath);
|
|
454
|
+
if (!directoryExists) continue;
|
|
455
|
+
await this.addDirectory(apiVersionFolderPath, apiVersion);
|
|
207
456
|
}
|
|
208
|
-
}
|
|
209
|
-
|
|
457
|
+
}
|
|
458
|
+
getCustomApi(customApiName) {
|
|
459
|
+
return this.customApis[customApiName];
|
|
460
|
+
}
|
|
461
|
+
async addDirectory(directoryPath, apiVersion) {
|
|
462
|
+
var _a2;
|
|
463
|
+
const entries = await fs.promises.readdir(directoryPath, {
|
|
464
|
+
withFileTypes: true
|
|
465
|
+
});
|
|
466
|
+
const isTsx2 = (_a2 = process.argv[1]) == null ? void 0 : _a2.endsWith(".ts");
|
|
467
|
+
const isTsNode2 = process.env.TS_NODE_DEV || process.env.TS_NODE_PROJECT;
|
|
468
|
+
const extension = isTsx2 || isTsNode2 ? "ts" : "js";
|
|
469
|
+
const shouldEndWith = `.api.${apiVersion}.${extension}`;
|
|
470
|
+
await Bluebird2.map(entries, async (entry) => {
|
|
471
|
+
if (entry.isFile()) {
|
|
472
|
+
if (entry.name.endsWith(shouldEndWith) === false) return;
|
|
473
|
+
try {
|
|
474
|
+
const importPath = `${path.join(directoryPath, entry.name)}`;
|
|
475
|
+
const ApiImport = await import(importPath);
|
|
476
|
+
const customApiClass = new ApiImport.default();
|
|
477
|
+
logger.info(`Registering custom API: ${ApiImport.default.name}`);
|
|
478
|
+
this.bindMethodsToInstance(customApiClass);
|
|
479
|
+
this.customApis[ApiImport.default.name] = customApiClass;
|
|
480
|
+
} catch (e) {
|
|
481
|
+
logger.error(e);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
bindMethodsToInstance(instance) {
|
|
487
|
+
const proto = Object.getPrototypeOf(instance);
|
|
488
|
+
Object.getOwnPropertyNames(proto).forEach((key) => {
|
|
489
|
+
const property = instance[key];
|
|
490
|
+
if (typeof property === "function" && key !== "constructor") {
|
|
491
|
+
instance[key] = property.bind(instance);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
var customApiFactory = new CustomApiFactory();
|
|
497
|
+
var customApiFactory_default = customApiFactory;
|
|
210
498
|
|
|
211
|
-
// src/restura/
|
|
212
|
-
import
|
|
213
|
-
import
|
|
214
|
-
import cookieParser from "cookie-parser";
|
|
215
|
-
import { createHash } from "crypto";
|
|
216
|
-
import * as express from "express";
|
|
217
|
-
import fs3 from "fs";
|
|
218
|
-
import path3 from "path";
|
|
219
|
-
import pg2 from "pg";
|
|
220
|
-
import * as prettier3 from "prettier";
|
|
499
|
+
// src/restura/generators/apiGenerator.ts
|
|
500
|
+
import { ObjectUtils, StringUtils } from "@redskytech/core-utils";
|
|
501
|
+
import prettier from "prettier";
|
|
221
502
|
|
|
222
503
|
// src/restura/sql/SqlUtils.ts
|
|
223
504
|
var SqlUtils = class _SqlUtils {
|
|
224
505
|
static convertDatabaseTypeToTypescript(type, value) {
|
|
225
506
|
type = type.toLocaleLowerCase();
|
|
226
507
|
if (type.startsWith("tinyint") || type.startsWith("boolean")) return "boolean";
|
|
227
|
-
if (type.indexOf("int") > -1 || type.startsWith("decimal") || type.startsWith("double") || type.startsWith("float"))
|
|
508
|
+
if (type.indexOf("int") > -1 || type.startsWith("decimal") || type.startsWith("double") || type.startsWith("float") || type.indexOf("serial") > -1 || type.startsWith("decimal") || type.startsWith("real") || type.startsWith("double precision") || type.startsWith("numeric"))
|
|
228
509
|
return "number";
|
|
229
510
|
if (type === "json") {
|
|
230
511
|
if (!value) return "object";
|
|
@@ -245,7 +526,7 @@ var SqlUtils = class _SqlUtils {
|
|
|
245
526
|
}
|
|
246
527
|
};
|
|
247
528
|
|
|
248
|
-
// src/restura/ResponseValidator.ts
|
|
529
|
+
// src/restura/validators/ResponseValidator.ts
|
|
249
530
|
var ResponseValidator = class _ResponseValidator {
|
|
250
531
|
constructor(schema) {
|
|
251
532
|
this.database = schema.database;
|
|
@@ -310,9 +591,9 @@ var ResponseValidator = class _ResponseValidator {
|
|
|
310
591
|
return { validator: "any" };
|
|
311
592
|
}
|
|
312
593
|
getTypeFromTable(selector, name) {
|
|
313
|
-
const
|
|
314
|
-
if (
|
|
315
|
-
const tableName =
|
|
594
|
+
const path5 = selector.split(".");
|
|
595
|
+
if (path5.length === 0 || path5.length > 2 || path5[0] === "") return { validator: "any", isOptional: false };
|
|
596
|
+
const tableName = path5.length == 2 ? path5[0] : name, columnName = path5.length == 2 ? path5[1] : path5[0];
|
|
316
597
|
const table = this.database.find((t) => t.name == tableName);
|
|
317
598
|
const column = table == null ? void 0 : table.columns.find((c) => c.name == columnName);
|
|
318
599
|
if (!table || !column) return { validator: "any", isOptional: false };
|
|
@@ -394,9 +675,7 @@ var ResponseValidator = class _ResponseValidator {
|
|
|
394
675
|
}
|
|
395
676
|
};
|
|
396
677
|
|
|
397
|
-
// src/restura/apiGenerator.ts
|
|
398
|
-
import { ObjectUtils, StringUtils } from "@redskytech/core-utils";
|
|
399
|
-
import prettier from "prettier";
|
|
678
|
+
// src/restura/generators/apiGenerator.ts
|
|
400
679
|
var ApiTree = class _ApiTree {
|
|
401
680
|
constructor(namespace, database) {
|
|
402
681
|
this.database = database;
|
|
@@ -494,7 +773,7 @@ var ApiTree = class _ApiTree {
|
|
|
494
773
|
break;
|
|
495
774
|
}
|
|
496
775
|
}
|
|
497
|
-
return `'${p.name}'${p.required ? "" : "?"}:${requestType}`;
|
|
776
|
+
return `'${p.name}'${p.required ? "" : "?"}:${requestType}${p.isNullable ? " | null" : ""}`;
|
|
498
777
|
}).join(";\n")}${ObjectUtils.isArrayWithData(route.request) ? ";" : ""}
|
|
499
778
|
`;
|
|
500
779
|
modelString += `}`;
|
|
@@ -508,30 +787,37 @@ var ApiTree = class _ApiTree {
|
|
|
508
787
|
return `export type Res = CustomTypes.${route.responseType}[]`;
|
|
509
788
|
else return `export type Res = CustomTypes.${route.responseType}`;
|
|
510
789
|
}
|
|
511
|
-
return `export interface Res ${this.getFields(route.response)}`;
|
|
790
|
+
return `export interface Res ${this.getFields(route.response, route.table, route.joins)}`;
|
|
512
791
|
}
|
|
513
|
-
getFields(fields) {
|
|
514
|
-
const nameFields = fields.map((f) => this.getNameAndType(f));
|
|
792
|
+
getFields(fields, routeBaseTable, joins) {
|
|
793
|
+
const nameFields = fields.map((f) => this.getNameAndType(f, routeBaseTable, joins));
|
|
515
794
|
const nested = `{
|
|
516
795
|
${nameFields.join(";\n ")}${ObjectUtils.isArrayWithData(nameFields) ? ";" : ""}
|
|
517
796
|
}`;
|
|
518
797
|
return nested;
|
|
519
798
|
}
|
|
520
|
-
getNameAndType(p) {
|
|
521
|
-
let responseType = "any",
|
|
799
|
+
getNameAndType(p, routeBaseTable, joins) {
|
|
800
|
+
let responseType = "any", isNullable = false, array = false;
|
|
522
801
|
if (p.selector) {
|
|
523
|
-
({ responseType,
|
|
802
|
+
({ responseType, isNullable } = this.getTypeFromTable(p.selector, p.name));
|
|
803
|
+
const selectorKey = p.selector.split(".")[0];
|
|
804
|
+
if (selectorKey !== routeBaseTable) {
|
|
805
|
+
const join = joins.find((j) => j.alias === selectorKey);
|
|
806
|
+
if (join && join.type !== "INNER") {
|
|
807
|
+
isNullable = true;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
524
810
|
} else if (p.subquery) {
|
|
525
|
-
responseType = this.getFields(p.subquery.properties);
|
|
811
|
+
responseType = this.getFields(p.subquery.properties, p.subquery.table, p.subquery.joins);
|
|
526
812
|
array = true;
|
|
527
813
|
}
|
|
528
|
-
return `${p.name}${
|
|
814
|
+
return `${p.name}:${responseType}${array ? "[]" : ""}${isNullable ? " | null" : ""}`;
|
|
529
815
|
}
|
|
530
816
|
getTypeFromTable(selector, name) {
|
|
531
|
-
const
|
|
532
|
-
if (
|
|
533
|
-
let tableName =
|
|
534
|
-
const columnName =
|
|
817
|
+
const path5 = selector.split(".");
|
|
818
|
+
if (path5.length === 0 || path5.length > 2 || path5[0] === "") return { responseType: "any", isNullable: false };
|
|
819
|
+
let tableName = path5.length == 2 ? path5[0] : name;
|
|
820
|
+
const columnName = path5.length == 2 ? path5[1] : path5[0];
|
|
535
821
|
let table = this.database.find((t) => t.name == tableName);
|
|
536
822
|
if (!table && tableName.includes("_")) {
|
|
537
823
|
const tableAliasSplit = tableName.split("_");
|
|
@@ -539,18 +825,19 @@ var ApiTree = class _ApiTree {
|
|
|
539
825
|
table = this.database.find((t) => t.name == tableName);
|
|
540
826
|
}
|
|
541
827
|
const column = table == null ? void 0 : table.columns.find((c) => c.name == columnName);
|
|
542
|
-
if (!table || !column) return { responseType: "any",
|
|
828
|
+
if (!table || !column) return { responseType: "any", isNullable: false };
|
|
543
829
|
return {
|
|
544
830
|
responseType: SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value),
|
|
545
|
-
|
|
831
|
+
isNullable: column.roles.length > 0 || column.isNullable
|
|
546
832
|
};
|
|
547
833
|
}
|
|
548
834
|
};
|
|
549
|
-
function pathToNamespaces(
|
|
550
|
-
return
|
|
835
|
+
function pathToNamespaces(path5) {
|
|
836
|
+
return path5.split("/").map((e) => StringUtils.toPascalCasing(e)).filter((e) => e);
|
|
551
837
|
}
|
|
552
|
-
function apiGenerator(schema
|
|
553
|
-
let apiString = `/** Auto generated file
|
|
838
|
+
function apiGenerator(schema) {
|
|
839
|
+
let apiString = `/** Auto generated file. DO NOT MODIFY **/
|
|
840
|
+
`;
|
|
554
841
|
const rootNamespace = ApiTree.createRootNode(schema.database);
|
|
555
842
|
for (const endpoint of schema.endpoints) {
|
|
556
843
|
const endpointNamespaces = pathToNamespaces(endpoint.baseUrl);
|
|
@@ -565,7 +852,7 @@ function apiGenerator(schema, schemaHash) {
|
|
|
565
852
|
apiString += `
|
|
566
853
|
|
|
567
854
|
declare namespace CustomTypes {
|
|
568
|
-
${schema.customTypes}
|
|
855
|
+
${schema.customTypes.join("\n")}
|
|
569
856
|
}`;
|
|
570
857
|
}
|
|
571
858
|
return prettier.format(apiString, __spreadValues({
|
|
@@ -580,79 +867,32 @@ function apiGenerator(schema, schemaHash) {
|
|
|
580
867
|
}));
|
|
581
868
|
}
|
|
582
869
|
|
|
583
|
-
// src/restura/
|
|
584
|
-
import fs from "fs";
|
|
585
|
-
import path from "path";
|
|
586
|
-
var CustomApiFactory = class {
|
|
587
|
-
constructor() {
|
|
588
|
-
this.customApis = {};
|
|
589
|
-
}
|
|
590
|
-
async loadApiFiles(baseFolderPath) {
|
|
591
|
-
const apiVersions = ["v1"];
|
|
592
|
-
for (const apiVersion of apiVersions) {
|
|
593
|
-
const apiVersionFolderPath = path.join(baseFolderPath, apiVersion);
|
|
594
|
-
if (!fs.existsSync(apiVersionFolderPath)) continue;
|
|
595
|
-
await this.addDirectory(apiVersionFolderPath, apiVersion);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
getCustomApi(customApiName) {
|
|
599
|
-
return this.customApis[customApiName];
|
|
600
|
-
}
|
|
601
|
-
async addDirectory(directoryPath, apiVersion) {
|
|
602
|
-
const entries = fs.readdirSync(directoryPath, {
|
|
603
|
-
withFileTypes: true
|
|
604
|
-
});
|
|
605
|
-
for (const entry of entries) {
|
|
606
|
-
if (entry.isFile()) {
|
|
607
|
-
if (entry.name.endsWith(`.api.${apiVersion}.js`) === false) continue;
|
|
608
|
-
try {
|
|
609
|
-
const importPath = `${path.join(directoryPath, entry.name)}`;
|
|
610
|
-
const ApiImport = await import(importPath);
|
|
611
|
-
const customApiClass = new ApiImport.default();
|
|
612
|
-
logger.info(`Registering custom API: ${ApiImport.default.name}`);
|
|
613
|
-
this.bindMethodsToInstance(customApiClass);
|
|
614
|
-
this.customApis[ApiImport.default.name] = customApiClass;
|
|
615
|
-
} catch (e) {
|
|
616
|
-
console.error(e);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
bindMethodsToInstance(instance) {
|
|
622
|
-
const proto = Object.getPrototypeOf(instance);
|
|
623
|
-
Object.getOwnPropertyNames(proto).forEach((key) => {
|
|
624
|
-
const property = instance[key];
|
|
625
|
-
if (typeof property === "function" && key !== "constructor") {
|
|
626
|
-
instance[key] = property.bind(instance);
|
|
627
|
-
}
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
};
|
|
631
|
-
var customApiFactory = new CustomApiFactory();
|
|
632
|
-
var customApiFactory_default = customApiFactory;
|
|
633
|
-
|
|
634
|
-
// src/restura/customTypeValidationGenerator.ts
|
|
870
|
+
// src/restura/generators/customTypeValidationGenerator.ts
|
|
635
871
|
import fs2 from "fs";
|
|
636
|
-
import * as TJS from "typescript-json-schema";
|
|
637
872
|
import path2, { resolve } from "path";
|
|
638
873
|
import tmp from "tmp";
|
|
639
|
-
import * as
|
|
874
|
+
import * as TJS from "typescript-json-schema";
|
|
640
875
|
function customTypeValidationGenerator(currentSchema) {
|
|
641
876
|
const schemaObject = {};
|
|
642
|
-
const customInterfaceNames = currentSchema.customTypes.
|
|
877
|
+
const customInterfaceNames = currentSchema.customTypes.map((customType) => {
|
|
878
|
+
const matches = customType.match(new RegExp("(?<=interface\\s)(\\w+)|(?<=type\\s)(\\w+)", "g"));
|
|
879
|
+
if (matches && matches.length > 0) return matches[0];
|
|
880
|
+
return "";
|
|
881
|
+
}).filter(Boolean);
|
|
643
882
|
if (!customInterfaceNames) return {};
|
|
644
883
|
const temporaryFile = tmp.fileSync({ mode: 420, prefix: "prefix-", postfix: ".ts" });
|
|
645
|
-
fs2.writeFileSync(temporaryFile.name, currentSchema.customTypes);
|
|
884
|
+
fs2.writeFileSync(temporaryFile.name, currentSchema.customTypes.join("\n"));
|
|
646
885
|
const compilerOptions = {
|
|
647
886
|
strictNullChecks: true,
|
|
648
887
|
skipLibCheck: true
|
|
888
|
+
// Needed if we are processing ES modules
|
|
649
889
|
};
|
|
650
890
|
const program = TJS.getProgramFromFiles(
|
|
651
891
|
[
|
|
652
892
|
resolve(temporaryFile.name),
|
|
653
|
-
|
|
654
|
-
path2.join(
|
|
655
|
-
path2.join(
|
|
893
|
+
path2.join(restura.resturaConfig.generatedTypesPath, "restura.d.ts"),
|
|
894
|
+
path2.join(restura.resturaConfig.generatedTypesPath, "models.d.ts"),
|
|
895
|
+
path2.join(restura.resturaConfig.generatedTypesPath, "api.d.ts")
|
|
656
896
|
],
|
|
657
897
|
compilerOptions
|
|
658
898
|
);
|
|
@@ -666,6 +906,61 @@ function customTypeValidationGenerator(currentSchema) {
|
|
|
666
906
|
return schemaObject;
|
|
667
907
|
}
|
|
668
908
|
|
|
909
|
+
// src/restura/generators/modelGenerator.ts
|
|
910
|
+
import { StringUtils as StringUtils2 } from "@redskytech/core-utils";
|
|
911
|
+
import prettier2 from "prettier";
|
|
912
|
+
function modelGenerator(schema) {
|
|
913
|
+
let modelString = `/** Auto generated file. DO NOT MODIFY **/
|
|
914
|
+
|
|
915
|
+
`;
|
|
916
|
+
modelString += `declare namespace Model {
|
|
917
|
+
`;
|
|
918
|
+
for (const table of schema.database) {
|
|
919
|
+
modelString += convertTable(table);
|
|
920
|
+
}
|
|
921
|
+
modelString += `}`;
|
|
922
|
+
return prettier2.format(modelString, __spreadValues({
|
|
923
|
+
parser: "typescript"
|
|
924
|
+
}, {
|
|
925
|
+
trailingComma: "none",
|
|
926
|
+
tabWidth: 4,
|
|
927
|
+
useTabs: true,
|
|
928
|
+
endOfLine: "lf",
|
|
929
|
+
printWidth: 120,
|
|
930
|
+
singleQuote: true
|
|
931
|
+
}));
|
|
932
|
+
}
|
|
933
|
+
function convertTable(table) {
|
|
934
|
+
let modelString = ` export interface ${StringUtils2.capitalizeFirst(table.name)} {
|
|
935
|
+
`;
|
|
936
|
+
for (const column of table.columns) {
|
|
937
|
+
modelString += ` ${column.name}: ${SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value)}${column.isNullable ? " | null" : ""};
|
|
938
|
+
`;
|
|
939
|
+
}
|
|
940
|
+
modelString += ` }
|
|
941
|
+
`;
|
|
942
|
+
return modelString;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/restura/generators/resturaGlobalTypesGenerator.ts
|
|
946
|
+
function resturaGlobalTypesGenerator() {
|
|
947
|
+
return `/** Auto generated file. DO NOT MODIFY **/
|
|
948
|
+
/** This file contains types that may be used in the CustomTypes of Restura **/
|
|
949
|
+
/** For example export interface MyPagedQuery extends Restura.PageQuery { } **/
|
|
950
|
+
|
|
951
|
+
declare namespace Restura {
|
|
952
|
+
export type StandardOrderTypes = 'ASC' | 'DESC' | 'RAND' | 'NONE';
|
|
953
|
+
export interface PageQuery {
|
|
954
|
+
page?: number;
|
|
955
|
+
perPage?: number;
|
|
956
|
+
sortBy?: string;
|
|
957
|
+
sortOrder?: StandardOrderTypes;
|
|
958
|
+
filter?: string;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
`;
|
|
962
|
+
}
|
|
963
|
+
|
|
669
964
|
// src/restura/middleware/addApiResponseFunctions.ts
|
|
670
965
|
function addApiResponseFunctions(req, res, next) {
|
|
671
966
|
res.sendData = function(data, statusCode = 200) {
|
|
@@ -698,16 +993,41 @@ function addApiResponseFunctions(req, res, next) {
|
|
|
698
993
|
function authenticateUser(applicationAuthenticateHandler) {
|
|
699
994
|
return (req, res, next) => {
|
|
700
995
|
applicationAuthenticateHandler(req, res, (userDetails) => {
|
|
701
|
-
req.requesterDetails = __spreadValues(
|
|
996
|
+
req.requesterDetails = __spreadValues({ host: req.hostname, ipAddress: req.ip || "" }, userDetails);
|
|
702
997
|
next();
|
|
703
998
|
});
|
|
704
999
|
};
|
|
705
1000
|
}
|
|
706
1001
|
|
|
707
|
-
// src/restura/
|
|
1002
|
+
// src/restura/middleware/getMulterUpload.ts
|
|
1003
|
+
import multer from "multer";
|
|
1004
|
+
import * as os from "os";
|
|
1005
|
+
import { extname } from "path";
|
|
1006
|
+
var OneHundredMB = 100 * 1024 * 1024;
|
|
1007
|
+
var commonUpload = null;
|
|
1008
|
+
var getMulterUpload = (directory) => {
|
|
1009
|
+
if (commonUpload) return commonUpload;
|
|
1010
|
+
const storage = multer.diskStorage({
|
|
1011
|
+
destination: directory || os.tmpdir(),
|
|
1012
|
+
filename: function(request, file, cb) {
|
|
1013
|
+
const extension = extname(file.originalname);
|
|
1014
|
+
const uniqueName = Date.now() + "-" + Math.round(Math.random() * 1e3);
|
|
1015
|
+
cb(null, `${uniqueName}${extension}`);
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
commonUpload = multer({
|
|
1019
|
+
storage,
|
|
1020
|
+
limits: {
|
|
1021
|
+
fileSize: OneHundredMB
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
return commonUpload;
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
// src/restura/schemas/resturaSchema.ts
|
|
708
1028
|
import { z as z3 } from "zod";
|
|
709
1029
|
|
|
710
|
-
// src/restura/
|
|
1030
|
+
// src/restura/schemas/validatorDataSchema.ts
|
|
711
1031
|
import { z as z2 } from "zod";
|
|
712
1032
|
var validatorDataSchemeValue = z2.union([z2.string(), z2.array(z2.string()), z2.number(), z2.array(z2.number())]);
|
|
713
1033
|
var validatorDataSchema = z2.object({
|
|
@@ -715,7 +1035,7 @@ var validatorDataSchema = z2.object({
|
|
|
715
1035
|
value: validatorDataSchemeValue
|
|
716
1036
|
}).strict();
|
|
717
1037
|
|
|
718
|
-
// src/restura/
|
|
1038
|
+
// src/restura/schemas/resturaSchema.ts
|
|
719
1039
|
var orderBySchema = z3.object({
|
|
720
1040
|
columnName: z3.string(),
|
|
721
1041
|
order: z3.enum(["ASC", "DESC"]),
|
|
@@ -728,7 +1048,7 @@ var groupBySchema = z3.object({
|
|
|
728
1048
|
var whereDataSchema = z3.object({
|
|
729
1049
|
tableName: z3.string().optional(),
|
|
730
1050
|
columnName: z3.string().optional(),
|
|
731
|
-
operator: z3.enum(["=", "<", ">", "<=", ">=", "!=", "LIKE", "IN", "NOT IN", "STARTS WITH", "ENDS WITH"]).optional(),
|
|
1051
|
+
operator: z3.enum(["=", "<", ">", "<=", ">=", "!=", "LIKE", "IN", "NOT IN", "STARTS WITH", "ENDS WITH", "IS", "IS NOT"]).optional(),
|
|
732
1052
|
value: z3.string().or(z3.number()).optional(),
|
|
733
1053
|
custom: z3.string().optional(),
|
|
734
1054
|
conjunction: z3.enum(["AND", "OR"]).optional()
|
|
@@ -748,6 +1068,7 @@ var joinDataSchema = z3.object({
|
|
|
748
1068
|
var requestDataSchema = z3.object({
|
|
749
1069
|
name: z3.string(),
|
|
750
1070
|
required: z3.boolean(),
|
|
1071
|
+
isNullable: z3.boolean().optional(),
|
|
751
1072
|
validator: z3.array(validatorDataSchema)
|
|
752
1073
|
}).strict();
|
|
753
1074
|
var responseDataSchema = z3.object({
|
|
@@ -833,6 +1154,12 @@ var postgresColumnDateTypesSchema = z3.enum([
|
|
|
833
1154
|
"INTERVAL"
|
|
834
1155
|
// time span
|
|
835
1156
|
]);
|
|
1157
|
+
var postgresColumnJsonTypesSchema = z3.enum([
|
|
1158
|
+
"JSON",
|
|
1159
|
+
// stores JSON data as raw text
|
|
1160
|
+
"JSONB"
|
|
1161
|
+
// stores JSON data in a binary format, optimized for query performance
|
|
1162
|
+
]);
|
|
836
1163
|
var mariaDbColumnNumericTypesSchema = z3.enum([
|
|
837
1164
|
"BOOLEAN",
|
|
838
1165
|
// 1-byte A synonym for "TINYINT(1)". Supported from version 1.2.0 onwards.
|
|
@@ -895,6 +1222,7 @@ var columnDataSchema = z3.object({
|
|
|
895
1222
|
postgresColumnNumericTypesSchema,
|
|
896
1223
|
postgresColumnStringTypesSchema,
|
|
897
1224
|
postgresColumnDateTypesSchema,
|
|
1225
|
+
postgresColumnJsonTypesSchema,
|
|
898
1226
|
mariaDbColumnNumericTypesSchema,
|
|
899
1227
|
mariaDbColumnStringTypesSchema,
|
|
900
1228
|
mariaDbColumnDateTypesSchema
|
|
@@ -946,7 +1274,8 @@ var tableDataSchema = z3.object({
|
|
|
946
1274
|
indexes: z3.array(indexDataSchema),
|
|
947
1275
|
foreignKeys: z3.array(foreignKeyDataSchema),
|
|
948
1276
|
checkConstraints: z3.array(checkConstraintDataSchema),
|
|
949
|
-
roles: z3.array(z3.string())
|
|
1277
|
+
roles: z3.array(z3.string()),
|
|
1278
|
+
notify: z3.union([z3.literal("ALL"), z3.array(z3.string())]).optional()
|
|
950
1279
|
}).strict();
|
|
951
1280
|
var endpointDataSchema = z3.object({
|
|
952
1281
|
name: z3.string(),
|
|
@@ -954,16 +1283,16 @@ var endpointDataSchema = z3.object({
|
|
|
954
1283
|
baseUrl: z3.string(),
|
|
955
1284
|
routes: z3.array(z3.union([standardRouteSchema, customRouteSchema]))
|
|
956
1285
|
}).strict();
|
|
957
|
-
var
|
|
1286
|
+
var resturaSchema = z3.object({
|
|
958
1287
|
database: z3.array(tableDataSchema),
|
|
959
1288
|
endpoints: z3.array(endpointDataSchema),
|
|
960
1289
|
globalParams: z3.array(z3.string()),
|
|
961
1290
|
roles: z3.array(z3.string()),
|
|
962
|
-
customTypes: z3.string()
|
|
1291
|
+
customTypes: z3.array(z3.string())
|
|
963
1292
|
}).strict();
|
|
964
1293
|
async function isSchemaValid(schemaToCheck) {
|
|
965
1294
|
try {
|
|
966
|
-
|
|
1295
|
+
resturaSchema.parse(schemaToCheck);
|
|
967
1296
|
return true;
|
|
968
1297
|
} catch (error) {
|
|
969
1298
|
logger.error(error);
|
|
@@ -971,12 +1300,12 @@ async function isSchemaValid(schemaToCheck) {
|
|
|
971
1300
|
}
|
|
972
1301
|
}
|
|
973
1302
|
|
|
974
|
-
// src/restura/
|
|
1303
|
+
// src/restura/validators/requestValidator.ts
|
|
975
1304
|
import { ObjectUtils as ObjectUtils2 } from "@redskytech/core-utils";
|
|
976
1305
|
import jsonschema from "jsonschema";
|
|
977
1306
|
import { z as z4 } from "zod";
|
|
978
1307
|
|
|
979
|
-
// src/restura/utils/
|
|
1308
|
+
// src/restura/utils/utils.ts
|
|
980
1309
|
function addQuotesToStrings(variable) {
|
|
981
1310
|
if (typeof variable === "string") {
|
|
982
1311
|
return `'${variable}'`;
|
|
@@ -987,9 +1316,20 @@ function addQuotesToStrings(variable) {
|
|
|
987
1316
|
return variable;
|
|
988
1317
|
}
|
|
989
1318
|
}
|
|
1319
|
+
function sortObjectKeysAlphabetically(obj) {
|
|
1320
|
+
if (Array.isArray(obj)) {
|
|
1321
|
+
return obj.map(sortObjectKeysAlphabetically);
|
|
1322
|
+
} else if (obj !== null && typeof obj === "object") {
|
|
1323
|
+
return Object.keys(obj).sort().reduce((sorted, key) => {
|
|
1324
|
+
sorted[key] = sortObjectKeysAlphabetically(obj[key]);
|
|
1325
|
+
return sorted;
|
|
1326
|
+
}, {});
|
|
1327
|
+
}
|
|
1328
|
+
return obj;
|
|
1329
|
+
}
|
|
990
1330
|
|
|
991
|
-
// src/restura/
|
|
992
|
-
function
|
|
1331
|
+
// src/restura/validators/requestValidator.ts
|
|
1332
|
+
function requestValidator(req, routeData, validationSchema) {
|
|
993
1333
|
const requestData = getRequestData(req);
|
|
994
1334
|
req.data = requestData;
|
|
995
1335
|
if (routeData.request === void 0) {
|
|
@@ -1023,6 +1363,7 @@ function validateRequestParams(req, routeData, validationSchema) {
|
|
|
1023
1363
|
});
|
|
1024
1364
|
}
|
|
1025
1365
|
function validateRequestSingleParam(requestValue, requestParam) {
|
|
1366
|
+
if (requestParam.isNullable && requestValue === null) return;
|
|
1026
1367
|
requestParam.validator.forEach((validator) => {
|
|
1027
1368
|
switch (validator.type) {
|
|
1028
1369
|
case "TYPE_CHECK":
|
|
@@ -1143,9 +1484,15 @@ function getRequestData(req) {
|
|
|
1143
1484
|
bodyData[attr] = attrList;
|
|
1144
1485
|
}
|
|
1145
1486
|
} else {
|
|
1146
|
-
bodyData[attr]
|
|
1147
|
-
|
|
1148
|
-
|
|
1487
|
+
if (bodyData[attr] === "true") {
|
|
1488
|
+
bodyData[attr] = true;
|
|
1489
|
+
} else if (bodyData[attr] === "false") {
|
|
1490
|
+
bodyData[attr] = false;
|
|
1491
|
+
} else {
|
|
1492
|
+
bodyData[attr] = ObjectUtils2.safeParse(bodyData[attr]);
|
|
1493
|
+
if (isNaN(Number(bodyData[attr]))) continue;
|
|
1494
|
+
bodyData[attr] = Number(bodyData[attr]);
|
|
1495
|
+
}
|
|
1149
1496
|
}
|
|
1150
1497
|
}
|
|
1151
1498
|
}
|
|
@@ -1156,7 +1503,7 @@ function getRequestData(req) {
|
|
|
1156
1503
|
async function schemaValidation(req, res, next) {
|
|
1157
1504
|
req.data = getRequestData(req);
|
|
1158
1505
|
try {
|
|
1159
|
-
|
|
1506
|
+
resturaSchema.parse(req.data);
|
|
1160
1507
|
next();
|
|
1161
1508
|
} catch (error) {
|
|
1162
1509
|
logger.error(error);
|
|
@@ -1164,43 +1511,34 @@ async function schemaValidation(req, res, next) {
|
|
|
1164
1511
|
}
|
|
1165
1512
|
}
|
|
1166
1513
|
|
|
1167
|
-
// src/restura/
|
|
1168
|
-
import {
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
}, {
|
|
1182
|
-
trailingComma: "none",
|
|
1183
|
-
tabWidth: 4,
|
|
1184
|
-
useTabs: true,
|
|
1185
|
-
endOfLine: "lf",
|
|
1186
|
-
printWidth: 120,
|
|
1187
|
-
singleQuote: true
|
|
1188
|
-
}));
|
|
1189
|
-
}
|
|
1190
|
-
function convertTable(table) {
|
|
1191
|
-
let modelString = ` export interface ${StringUtils2.capitalizeFirst(table.name)} {
|
|
1192
|
-
`;
|
|
1193
|
-
for (const column of table.columns) {
|
|
1194
|
-
modelString += ` ${column.name}${column.isNullable ? "?" : ""}: ${SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value)};
|
|
1195
|
-
`;
|
|
1196
|
-
}
|
|
1197
|
-
modelString += ` }
|
|
1198
|
-
`;
|
|
1199
|
-
return modelString;
|
|
1200
|
-
}
|
|
1514
|
+
// src/restura/schemas/resturaConfigSchema.ts
|
|
1515
|
+
import { z as z5 } from "zod";
|
|
1516
|
+
var _a;
|
|
1517
|
+
var isTsx = (_a = process.argv[1]) == null ? void 0 : _a.endsWith(".ts");
|
|
1518
|
+
var isTsNode = process.env.TS_NODE_DEV || process.env.TS_NODE_PROJECT;
|
|
1519
|
+
var customApiFolderPath = isTsx || isTsNode ? "/src/api" : "/dist/api";
|
|
1520
|
+
var resturaConfigSchema = z5.object({
|
|
1521
|
+
authToken: z5.string().min(1, "Missing Restura Auth Token"),
|
|
1522
|
+
sendErrorStackTrace: z5.boolean().default(false),
|
|
1523
|
+
schemaFilePath: z5.string().default(process.cwd() + "/restura.schema.json"),
|
|
1524
|
+
customApiFolderPath: z5.string().default(process.cwd() + customApiFolderPath),
|
|
1525
|
+
generatedTypesPath: z5.string().default(process.cwd() + "/src/@types"),
|
|
1526
|
+
fileTempCachePath: z5.string().optional()
|
|
1527
|
+
});
|
|
1201
1528
|
|
|
1202
1529
|
// src/restura/sql/PsqlEngine.ts
|
|
1203
1530
|
import { ObjectUtils as ObjectUtils4 } from "@redskytech/core-utils";
|
|
1531
|
+
import getDiff from "@wmfs/pg-diff-sync";
|
|
1532
|
+
import pgInfo from "@wmfs/pg-info";
|
|
1533
|
+
import pg2 from "pg";
|
|
1534
|
+
|
|
1535
|
+
// src/restura/sql/PsqlPool.ts
|
|
1536
|
+
import pg from "pg";
|
|
1537
|
+
|
|
1538
|
+
// src/restura/sql/PsqlConnection.ts
|
|
1539
|
+
import crypto from "crypto";
|
|
1540
|
+
import format3 from "pg-format";
|
|
1541
|
+
import { format as sqlFormat } from "sql-formatter";
|
|
1204
1542
|
|
|
1205
1543
|
// src/restura/sql/PsqlUtils.ts
|
|
1206
1544
|
import format2 from "pg-format";
|
|
@@ -1210,16 +1548,33 @@ function escapeColumnName(columnName) {
|
|
|
1210
1548
|
}
|
|
1211
1549
|
function questionMarksToOrderedParams(query) {
|
|
1212
1550
|
let count = 1;
|
|
1213
|
-
|
|
1551
|
+
let inSingleQuote = false;
|
|
1552
|
+
let inDoubleQuote = false;
|
|
1553
|
+
return query.replace(/('|"|\?)/g, (char) => {
|
|
1554
|
+
if (char === "'") {
|
|
1555
|
+
inSingleQuote = !inSingleQuote && !inDoubleQuote;
|
|
1556
|
+
return char;
|
|
1557
|
+
}
|
|
1558
|
+
if (char === '"') {
|
|
1559
|
+
inDoubleQuote = !inDoubleQuote && !inSingleQuote;
|
|
1560
|
+
return char;
|
|
1561
|
+
}
|
|
1562
|
+
if (char === "?" && !inSingleQuote && !inDoubleQuote) {
|
|
1563
|
+
return `$${count++}`;
|
|
1564
|
+
}
|
|
1565
|
+
return char;
|
|
1566
|
+
});
|
|
1214
1567
|
}
|
|
1215
1568
|
function insertObjectQuery(table, obj) {
|
|
1216
1569
|
const keys = Object.keys(obj);
|
|
1217
1570
|
const params = Object.values(obj);
|
|
1218
1571
|
const columns = keys.map((column) => escapeColumnName(column)).join(", ");
|
|
1219
1572
|
const values = params.map((value) => SQL`${value}`).join(", ");
|
|
1220
|
-
|
|
1573
|
+
let query = `
|
|
1574
|
+
INSERT INTO "${table}" (${columns})
|
|
1221
1575
|
VALUES (${values})
|
|
1222
1576
|
RETURNING *`;
|
|
1577
|
+
query = query.replace(/'(\?)'/, "?");
|
|
1223
1578
|
return query;
|
|
1224
1579
|
}
|
|
1225
1580
|
function updateObjectQuery(table, obj, whereStatement) {
|
|
@@ -1227,25 +1582,132 @@ function updateObjectQuery(table, obj, whereStatement) {
|
|
|
1227
1582
|
for (const i in obj) {
|
|
1228
1583
|
setArray.push(`${escapeColumnName(i)} = ` + SQL`${obj[i]}`);
|
|
1229
1584
|
}
|
|
1230
|
-
return `
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1585
|
+
return `
|
|
1586
|
+
UPDATE ${escapeColumnName(table)}
|
|
1587
|
+
SET ${setArray.join(", ")} ${whereStatement}
|
|
1588
|
+
RETURNING *`;
|
|
1589
|
+
}
|
|
1590
|
+
function isValueNumber2(value) {
|
|
1591
|
+
return !isNaN(Number(value));
|
|
1592
|
+
}
|
|
1593
|
+
function SQL(strings, ...values) {
|
|
1594
|
+
let query = strings[0];
|
|
1595
|
+
values.forEach((value, index) => {
|
|
1596
|
+
if (typeof value === "boolean") {
|
|
1597
|
+
query += value;
|
|
1598
|
+
} else if (typeof value === "number") {
|
|
1599
|
+
query += value;
|
|
1600
|
+
} else if (Array.isArray(value)) {
|
|
1601
|
+
query += format2.literal(JSON.stringify(value)) + "::jsonb";
|
|
1602
|
+
} else {
|
|
1603
|
+
query += format2.literal(value);
|
|
1604
|
+
}
|
|
1605
|
+
query += strings[index + 1];
|
|
1606
|
+
});
|
|
1607
|
+
return query;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// src/restura/sql/PsqlConnection.ts
|
|
1611
|
+
var PsqlConnection = class {
|
|
1612
|
+
constructor(instanceId) {
|
|
1613
|
+
this.instanceId = instanceId || crypto.randomUUID();
|
|
1614
|
+
}
|
|
1615
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1616
|
+
async queryOne(query, options, requesterDetails) {
|
|
1617
|
+
const formattedQuery = questionMarksToOrderedParams(query);
|
|
1618
|
+
const meta = __spreadValues({ connectionInstanceId: this.instanceId }, requesterDetails);
|
|
1619
|
+
this.logSqlStatement(formattedQuery, options, meta);
|
|
1620
|
+
const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
|
|
1621
|
+
`;
|
|
1622
|
+
const startTime = process.hrtime();
|
|
1623
|
+
try {
|
|
1624
|
+
const response = await this.query(queryMetadata + formattedQuery, options);
|
|
1625
|
+
this.logQueryDuration(startTime);
|
|
1626
|
+
if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
|
|
1627
|
+
else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
|
|
1628
|
+
return response.rows[0];
|
|
1629
|
+
} catch (error) {
|
|
1630
|
+
if (RsError.isRsError(error)) throw error;
|
|
1631
|
+
if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
|
|
1632
|
+
throw new RsError("DUPLICATE", error.message);
|
|
1633
|
+
}
|
|
1634
|
+
throw new RsError("DATABASE_ERROR", `${error.message}`);
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1638
|
+
async runQuery(query, options, requesterDetails) {
|
|
1639
|
+
const formattedQuery = questionMarksToOrderedParams(query);
|
|
1640
|
+
const meta = __spreadValues({ connectionInstanceId: this.instanceId }, requesterDetails);
|
|
1641
|
+
this.logSqlStatement(formattedQuery, options, meta);
|
|
1642
|
+
const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
|
|
1643
|
+
`;
|
|
1644
|
+
const startTime = process.hrtime();
|
|
1645
|
+
try {
|
|
1646
|
+
const response = await this.query(queryMetadata + formattedQuery, options);
|
|
1647
|
+
this.logQueryDuration(startTime);
|
|
1648
|
+
return response.rows;
|
|
1649
|
+
} catch (error) {
|
|
1650
|
+
if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
|
|
1651
|
+
throw new RsError("DUPLICATE", error.message);
|
|
1652
|
+
}
|
|
1653
|
+
throw new RsError("DATABASE_ERROR", `${error.message}`);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
logQueryDuration(startTime) {
|
|
1657
|
+
if (logger.level === "silly") {
|
|
1658
|
+
const [seconds, nanoseconds] = process.hrtime(startTime);
|
|
1659
|
+
const duration = seconds * 1e3 + nanoseconds / 1e6;
|
|
1660
|
+
logger.silly(`Query duration: ${duration.toFixed(2)}ms`);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
logSqlStatement(query, options, queryMetadata, prefix = "") {
|
|
1664
|
+
if (logger.level !== "silly") return;
|
|
1665
|
+
let sqlStatement = "";
|
|
1666
|
+
if (options.length === 0) {
|
|
1667
|
+
sqlStatement = query;
|
|
1242
1668
|
} else {
|
|
1243
|
-
|
|
1669
|
+
let stringIndex = 0;
|
|
1670
|
+
sqlStatement = query.replace(/\$\d+/g, () => {
|
|
1671
|
+
const value = options[stringIndex++];
|
|
1672
|
+
if (typeof value === "number") return value.toString();
|
|
1673
|
+
return format3.literal(value);
|
|
1674
|
+
});
|
|
1244
1675
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1676
|
+
const formattedSql = sqlFormat(sqlStatement, {
|
|
1677
|
+
language: "postgresql",
|
|
1678
|
+
linesBetweenQueries: 2,
|
|
1679
|
+
indentStyle: "standard",
|
|
1680
|
+
keywordCase: "upper",
|
|
1681
|
+
useTabs: true,
|
|
1682
|
+
tabWidth: 4
|
|
1683
|
+
});
|
|
1684
|
+
let initiator = "Anonymous";
|
|
1685
|
+
if ("userId" in queryMetadata && queryMetadata.userId)
|
|
1686
|
+
initiator = `User Id (${queryMetadata.userId.toString()})`;
|
|
1687
|
+
if ("isSystemUser" in queryMetadata && queryMetadata.isSystemUser) initiator = "SYSTEM";
|
|
1688
|
+
logger.silly(`${prefix}query by ${initiator}, Query ->
|
|
1689
|
+
${formattedSql}`);
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
|
|
1693
|
+
// src/restura/sql/PsqlPool.ts
|
|
1694
|
+
var { Pool } = pg;
|
|
1695
|
+
var PsqlPool = class extends PsqlConnection {
|
|
1696
|
+
constructor(poolConfig) {
|
|
1697
|
+
super();
|
|
1698
|
+
this.poolConfig = poolConfig;
|
|
1699
|
+
this.pool = new Pool(poolConfig);
|
|
1700
|
+
this.queryOne("SELECT NOW();", [], { isSystemUser: true, role: "", host: "localhost", ipAddress: "" }).then(() => {
|
|
1701
|
+
logger.info("Connected to PostgreSQL database");
|
|
1702
|
+
}).catch((error) => {
|
|
1703
|
+
logger.error("Error connecting to database", error);
|
|
1704
|
+
process.exit(1);
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
async query(query, values) {
|
|
1708
|
+
return this.pool.query(query, values);
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1249
1711
|
|
|
1250
1712
|
// src/restura/sql/SqlEngine.ts
|
|
1251
1713
|
import { ObjectUtils as ObjectUtils3 } from "@redskytech/core-utils";
|
|
@@ -1313,11 +1775,11 @@ var SqlEngine = class {
|
|
|
1313
1775
|
return returnValue;
|
|
1314
1776
|
}
|
|
1315
1777
|
replaceLocalParamKeywords(value, routeData, req, sqlParams) {
|
|
1316
|
-
var
|
|
1778
|
+
var _a2;
|
|
1317
1779
|
if (!routeData.request) return value;
|
|
1318
1780
|
const data = req.data;
|
|
1319
1781
|
if (typeof value === "string") {
|
|
1320
|
-
(
|
|
1782
|
+
(_a2 = value.match(/\$[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a2.forEach((param) => {
|
|
1321
1783
|
const requestParam = routeData.request.find((item) => {
|
|
1322
1784
|
return item.name === param.replace("$", "");
|
|
1323
1785
|
});
|
|
@@ -1330,9 +1792,9 @@ var SqlEngine = class {
|
|
|
1330
1792
|
return value;
|
|
1331
1793
|
}
|
|
1332
1794
|
replaceGlobalParamKeywords(value, routeData, req, sqlParams) {
|
|
1333
|
-
var
|
|
1795
|
+
var _a2;
|
|
1334
1796
|
if (typeof value === "string") {
|
|
1335
|
-
(
|
|
1797
|
+
(_a2 = value.match(/#[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a2.forEach((param) => {
|
|
1336
1798
|
param = param.replace("#", "");
|
|
1337
1799
|
const globalParamValue = req.requesterDetails[param];
|
|
1338
1800
|
if (!globalParamValue)
|
|
@@ -1351,32 +1813,86 @@ var SqlEngine = class {
|
|
|
1351
1813
|
// src/restura/sql/filterPsqlParser.ts
|
|
1352
1814
|
import peg from "pegjs";
|
|
1353
1815
|
var filterSqlGrammar = `
|
|
1816
|
+
{
|
|
1817
|
+
// ported from pg-format but intentionally will add double quotes to every column
|
|
1818
|
+
function quoteSqlIdentity(value) {
|
|
1819
|
+
if (value === undefined || value === null) {
|
|
1820
|
+
throw new Error('SQL identifier cannot be null or undefined');
|
|
1821
|
+
} else if (value === false) {
|
|
1822
|
+
return '"f"';
|
|
1823
|
+
} else if (value === true) {
|
|
1824
|
+
return '"t"';
|
|
1825
|
+
} else if (value instanceof Date) {
|
|
1826
|
+
// return '"' + formatDate(value.toISOString()) + '"';
|
|
1827
|
+
} else if (value instanceof Buffer) {
|
|
1828
|
+
throw new Error('SQL identifier cannot be a buffer');
|
|
1829
|
+
} else if (Array.isArray(value) === true) {
|
|
1830
|
+
var temp = [];
|
|
1831
|
+
for (var i = 0; i < value.length; i++) {
|
|
1832
|
+
if (Array.isArray(value[i]) === true) {
|
|
1833
|
+
throw new Error('Nested array to grouped list conversion is not supported for SQL identifier');
|
|
1834
|
+
} else {
|
|
1835
|
+
// temp.push(quoteIdent(value[i]));
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return temp.toString();
|
|
1839
|
+
} else if (value === Object(value)) {
|
|
1840
|
+
throw new Error('SQL identifier cannot be an object');
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
var ident = value.toString().slice(0); // create copy
|
|
1844
|
+
|
|
1845
|
+
// do not quote a valid, unquoted identifier
|
|
1846
|
+
// if (/^[a-z_][a-z0-9_$]*$/.test(ident) === true && isReserved(ident) === false) {
|
|
1847
|
+
// return ident;
|
|
1848
|
+
// }
|
|
1849
|
+
|
|
1850
|
+
var quoted = '"';
|
|
1851
|
+
|
|
1852
|
+
for (var i = 0; i < ident.length; i++) {
|
|
1853
|
+
var c = ident[i];
|
|
1854
|
+
if (c === '"') {
|
|
1855
|
+
quoted += c + c;
|
|
1856
|
+
} else {
|
|
1857
|
+
quoted += c;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
quoted += '"';
|
|
1862
|
+
|
|
1863
|
+
return quoted;
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1354
1867
|
start = expressionList
|
|
1355
1868
|
|
|
1869
|
+
_ = [ \\t\\r\\n]* // Matches spaces, tabs, and line breaks
|
|
1870
|
+
|
|
1356
1871
|
expressionList =
|
|
1357
|
-
|
|
1872
|
+
leftExpression:expression _ operator:operator _ rightExpression:expressionList
|
|
1358
1873
|
{ return \`\${leftExpression} \${operator} \${rightExpression}\`;}
|
|
1359
1874
|
/ expression
|
|
1360
1875
|
|
|
1361
1876
|
expression =
|
|
1362
|
-
negate:negate?"(" "column:"
|
|
1363
|
-
{return \`\${negate? "
|
|
1877
|
+
negate:negate? _ "(" _ "column" _ ":" column:column _ ","? _ value:value? ","? _ type:type? _ ")"_
|
|
1878
|
+
{return \`\${negate? " NOT " : ""}(\${type? type(column, value) : \`\${column} = \${format.literal(value)}\`})\`;}
|
|
1364
1879
|
/
|
|
1365
|
-
negate:negate?"("expression:expressionList")" { return \`\${negate? "
|
|
1880
|
+
negate:negate?"("expression:expressionList")" { return \`\${negate? " NOT " : ""}(\${expression})\`; }
|
|
1366
1881
|
|
|
1367
1882
|
negate = "!"
|
|
1368
1883
|
|
|
1369
1884
|
operator = "and"i / "or"i
|
|
1370
1885
|
|
|
1371
1886
|
|
|
1372
|
-
column = left:text "." right:text { return \`\${
|
|
1887
|
+
column = left:text "." right:text { return \`\${quoteSqlIdentity(left)}.\${quoteSqlIdentity(right)}\`; }
|
|
1373
1888
|
/
|
|
1374
|
-
text:text { return
|
|
1889
|
+
text:text { return quoteSqlIdentity(text); }
|
|
1375
1890
|
|
|
1376
1891
|
|
|
1377
|
-
text = text:[a-z0-9
|
|
1892
|
+
text = text:[a-z0-9 \\t\\r\\n\\-_:@']i+ { return text.join(""); }
|
|
1378
1893
|
|
|
1379
|
-
|
|
1894
|
+
|
|
1895
|
+
type = "type" _ ":" _ type:typeString { return type; }
|
|
1380
1896
|
typeString = text:"startsWith" { return function(column, value) { return \`\${column} ILIKE '\${format.literal(value).slice(1,-1)}%'\`; } } /
|
|
1381
1897
|
text:"endsWith" { return function(column, value) { return \`\${column} ILIKE '%\${format.literal(value).slice(1,-1)}'\`; } } /
|
|
1382
1898
|
text:"contains" { return function(column, value) { return \`\${column} ILIKE '%\${format.literal(value).slice(1,-1)}%'\`; } } /
|
|
@@ -1386,8 +1902,9 @@ typeString = text:"startsWith" { return function(column, value) { return \`\${co
|
|
|
1386
1902
|
text:"lessThanEqual" { return function(column, value) { return \`\${column} <= '\${format.literal(value).slice(1,-1)}'\`; } } /
|
|
1387
1903
|
text:"lessThan" { return function(column, value) { return \`\${column} < '\${format.literal(value).slice(1,-1)}'\`; } } /
|
|
1388
1904
|
text:"isNull" { return function(column, value) { return \`isNull(\${column})\`; } }
|
|
1389
|
-
|
|
1390
|
-
value = "value:" value:text { return value; }
|
|
1905
|
+
|
|
1906
|
+
value = "value" _ ":" value:text { return value; }
|
|
1907
|
+
|
|
1391
1908
|
|
|
1392
1909
|
`;
|
|
1393
1910
|
var filterPsqlParser = peg.generate(filterSqlGrammar, {
|
|
@@ -1397,18 +1914,224 @@ var filterPsqlParser = peg.generate(filterSqlGrammar, {
|
|
|
1397
1914
|
var filterPsqlParser_default = filterPsqlParser;
|
|
1398
1915
|
|
|
1399
1916
|
// src/restura/sql/PsqlEngine.ts
|
|
1917
|
+
var { Client, types } = pg2;
|
|
1918
|
+
var systemUser = {
|
|
1919
|
+
role: "",
|
|
1920
|
+
host: "",
|
|
1921
|
+
ipAddress: "",
|
|
1922
|
+
isSystemUser: true
|
|
1923
|
+
};
|
|
1400
1924
|
var PsqlEngine = class extends SqlEngine {
|
|
1401
|
-
constructor(psqlConnectionPool) {
|
|
1925
|
+
constructor(psqlConnectionPool, shouldListenForDbTriggers = false) {
|
|
1402
1926
|
super();
|
|
1403
1927
|
this.psqlConnectionPool = psqlConnectionPool;
|
|
1928
|
+
this.setupPgReturnTypes();
|
|
1929
|
+
if (shouldListenForDbTriggers) {
|
|
1930
|
+
this.setupTriggerListeners = this.listenForDbTriggers();
|
|
1931
|
+
}
|
|
1404
1932
|
}
|
|
1405
|
-
async
|
|
1406
|
-
|
|
1407
|
-
|
|
1933
|
+
async close() {
|
|
1934
|
+
if (this.triggerClient) {
|
|
1935
|
+
await this.triggerClient.end();
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
setupPgReturnTypes() {
|
|
1939
|
+
const TIMESTAMPTZ_OID = 1184;
|
|
1940
|
+
types.setTypeParser(TIMESTAMPTZ_OID, (val) => {
|
|
1941
|
+
return val === null ? null : new Date(val).toISOString();
|
|
1942
|
+
});
|
|
1943
|
+
const BIGINT_OID = 20;
|
|
1944
|
+
types.setTypeParser(BIGINT_OID, (val) => {
|
|
1945
|
+
return val === null ? null : Number(val);
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
async listenForDbTriggers() {
|
|
1949
|
+
this.triggerClient = new Client({
|
|
1950
|
+
user: this.psqlConnectionPool.poolConfig.user,
|
|
1951
|
+
host: this.psqlConnectionPool.poolConfig.host,
|
|
1952
|
+
database: this.psqlConnectionPool.poolConfig.database,
|
|
1953
|
+
password: this.psqlConnectionPool.poolConfig.password,
|
|
1954
|
+
port: this.psqlConnectionPool.poolConfig.port,
|
|
1955
|
+
connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
|
|
1956
|
+
});
|
|
1957
|
+
await this.triggerClient.connect();
|
|
1958
|
+
const promises = [];
|
|
1959
|
+
promises.push(this.triggerClient.query("LISTEN insert"));
|
|
1960
|
+
promises.push(this.triggerClient.query("LISTEN update"));
|
|
1961
|
+
promises.push(this.triggerClient.query("LISTEN delete"));
|
|
1962
|
+
await Promise.all(promises);
|
|
1963
|
+
this.triggerClient.on("notification", async (msg) => {
|
|
1964
|
+
if (msg.channel === "insert" || msg.channel === "update" || msg.channel === "delete") {
|
|
1965
|
+
const payload = ObjectUtils4.safeParse(msg.payload);
|
|
1966
|
+
await this.handleTrigger(payload, msg.channel.toUpperCase());
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
async handleTrigger(payload, mutationType) {
|
|
1971
|
+
if (payload.queryMetadata && payload.queryMetadata.connectionInstanceId === this.psqlConnectionPool.instanceId) {
|
|
1972
|
+
await eventManager_default.fireActionFromDbTrigger({ queryMetadata: payload.queryMetadata, mutationType }, payload);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
async createDatabaseFromSchema(schema, connection) {
|
|
1976
|
+
const sqlFullStatement = this.generateDatabaseSchemaFromSchema(schema);
|
|
1977
|
+
await connection.runQuery(sqlFullStatement, [], systemUser);
|
|
1978
|
+
return sqlFullStatement;
|
|
1408
1979
|
}
|
|
1409
1980
|
generateDatabaseSchemaFromSchema(schema) {
|
|
1410
|
-
|
|
1411
|
-
|
|
1981
|
+
const sqlStatements = [];
|
|
1982
|
+
const indexes = [];
|
|
1983
|
+
const triggers = [];
|
|
1984
|
+
for (const table of schema.database) {
|
|
1985
|
+
if (table.notify) {
|
|
1986
|
+
triggers.push(this.createInsertTriggers(table.name, table.notify));
|
|
1987
|
+
triggers.push(this.createUpdateTrigger(table.name, table.notify));
|
|
1988
|
+
triggers.push(this.createDeleteTrigger(table.name, table.notify));
|
|
1989
|
+
}
|
|
1990
|
+
let sql = `CREATE TABLE "${table.name}"
|
|
1991
|
+
( `;
|
|
1992
|
+
const tableColumns = [];
|
|
1993
|
+
for (const column of table.columns) {
|
|
1994
|
+
let columnSql = "";
|
|
1995
|
+
columnSql += ` "${column.name}" ${this.schemaToPsqlType(column)}`;
|
|
1996
|
+
let value = column.value;
|
|
1997
|
+
if (column.type === "JSON") value = "";
|
|
1998
|
+
if (column.type === "JSONB") value = "";
|
|
1999
|
+
if (column.type === "DECIMAL" && value) {
|
|
2000
|
+
value = value.replace("-", ",").replace(/['"]/g, "");
|
|
2001
|
+
}
|
|
2002
|
+
if (value && column.type !== "ENUM") {
|
|
2003
|
+
columnSql += `(${value})`;
|
|
2004
|
+
} else if (column.length) columnSql += `(${column.length})`;
|
|
2005
|
+
if (column.isPrimary) {
|
|
2006
|
+
columnSql += " PRIMARY KEY ";
|
|
2007
|
+
}
|
|
2008
|
+
if (column.isUnique) {
|
|
2009
|
+
columnSql += ` CONSTRAINT "${table.name}_${column.name}_unique_index" UNIQUE `;
|
|
2010
|
+
}
|
|
2011
|
+
if (column.isNullable) columnSql += " NULL";
|
|
2012
|
+
else columnSql += " NOT NULL";
|
|
2013
|
+
if (column.default) columnSql += ` DEFAULT ${column.default}`;
|
|
2014
|
+
if (value && column.type === "ENUM") {
|
|
2015
|
+
columnSql += ` CHECK ("${column.name}" IN (${value}))`;
|
|
2016
|
+
}
|
|
2017
|
+
tableColumns.push(columnSql);
|
|
2018
|
+
}
|
|
2019
|
+
sql += tableColumns.join(", \n");
|
|
2020
|
+
for (const index of table.indexes) {
|
|
2021
|
+
if (!index.isPrimaryKey) {
|
|
2022
|
+
let unique = " ";
|
|
2023
|
+
if (index.isUnique) unique = "UNIQUE ";
|
|
2024
|
+
indexes.push(
|
|
2025
|
+
` CREATE ${unique}INDEX "${index.name}" ON "${table.name}" (${index.columns.map((item) => {
|
|
2026
|
+
return `"${item}" ${index.order}`;
|
|
2027
|
+
}).join(", ")});`
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
sql += "\n);";
|
|
2032
|
+
sqlStatements.push(sql);
|
|
2033
|
+
}
|
|
2034
|
+
for (const table of schema.database) {
|
|
2035
|
+
if (!table.foreignKeys.length) continue;
|
|
2036
|
+
const sql = `ALTER TABLE "${table.name}" `;
|
|
2037
|
+
const constraints = [];
|
|
2038
|
+
for (const foreignKey of table.foreignKeys) {
|
|
2039
|
+
let constraint = ` ADD CONSTRAINT "${foreignKey.name}"
|
|
2040
|
+
FOREIGN KEY ("${foreignKey.column}") REFERENCES "${foreignKey.refTable}" ("${foreignKey.refColumn}")`;
|
|
2041
|
+
constraint += ` ON DELETE ${foreignKey.onDelete}`;
|
|
2042
|
+
constraint += ` ON UPDATE ${foreignKey.onUpdate}`;
|
|
2043
|
+
constraints.push(constraint);
|
|
2044
|
+
}
|
|
2045
|
+
sqlStatements.push(sql + constraints.join(",\n") + ";");
|
|
2046
|
+
}
|
|
2047
|
+
for (const table of schema.database) {
|
|
2048
|
+
if (!table.checkConstraints.length) continue;
|
|
2049
|
+
const sql = `ALTER TABLE "${table.name}" `;
|
|
2050
|
+
const constraints = [];
|
|
2051
|
+
for (const check of table.checkConstraints) {
|
|
2052
|
+
const constraint = `ADD CONSTRAINT "${check.name}" CHECK (${check.check})`;
|
|
2053
|
+
constraints.push(constraint);
|
|
2054
|
+
}
|
|
2055
|
+
sqlStatements.push(sql + constraints.join(",\n") + ";");
|
|
2056
|
+
}
|
|
2057
|
+
sqlStatements.push(indexes.join("\n"));
|
|
2058
|
+
sqlStatements.push(triggers.join("\n"));
|
|
2059
|
+
return sqlStatements.join("\n\n");
|
|
2060
|
+
}
|
|
2061
|
+
async getScratchPool() {
|
|
2062
|
+
var _a2, _b;
|
|
2063
|
+
const scratchDbExists = await this.psqlConnectionPool.runQuery(
|
|
2064
|
+
`SELECT *
|
|
2065
|
+
FROM pg_database
|
|
2066
|
+
WHERE datname = '${this.psqlConnectionPool.poolConfig.database}_scratch';`,
|
|
2067
|
+
[],
|
|
2068
|
+
systemUser
|
|
2069
|
+
);
|
|
2070
|
+
if (scratchDbExists.length === 0) {
|
|
2071
|
+
await this.psqlConnectionPool.runQuery(
|
|
2072
|
+
`CREATE DATABASE ${this.psqlConnectionPool.poolConfig.database}_scratch;`,
|
|
2073
|
+
[],
|
|
2074
|
+
systemUser
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
const scratchPool = new PsqlPool({
|
|
2078
|
+
host: this.psqlConnectionPool.poolConfig.host,
|
|
2079
|
+
port: this.psqlConnectionPool.poolConfig.port,
|
|
2080
|
+
user: this.psqlConnectionPool.poolConfig.user,
|
|
2081
|
+
database: this.psqlConnectionPool.poolConfig.database + "_scratch",
|
|
2082
|
+
password: this.psqlConnectionPool.poolConfig.password,
|
|
2083
|
+
max: this.psqlConnectionPool.poolConfig.max,
|
|
2084
|
+
idleTimeoutMillis: this.psqlConnectionPool.poolConfig.idleTimeoutMillis,
|
|
2085
|
+
connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
|
|
2086
|
+
});
|
|
2087
|
+
await scratchPool.runQuery(`DROP SCHEMA public CASCADE;`, [], systemUser);
|
|
2088
|
+
await scratchPool.runQuery(
|
|
2089
|
+
`CREATE SCHEMA public AUTHORIZATION ${this.psqlConnectionPool.poolConfig.user};`,
|
|
2090
|
+
[],
|
|
2091
|
+
systemUser
|
|
2092
|
+
);
|
|
2093
|
+
const schemaComment = await this.psqlConnectionPool.runQuery(
|
|
2094
|
+
`SELECT pg_description.description
|
|
2095
|
+
FROM pg_description
|
|
2096
|
+
JOIN pg_namespace ON pg_namespace.oid = pg_description.objoid
|
|
2097
|
+
WHERE pg_namespace.nspname = 'public';`,
|
|
2098
|
+
[],
|
|
2099
|
+
systemUser
|
|
2100
|
+
);
|
|
2101
|
+
if ((_a2 = schemaComment[0]) == null ? void 0 : _a2.description) {
|
|
2102
|
+
await scratchPool.runQuery(
|
|
2103
|
+
`COMMENT ON SCHEMA public IS '${(_b = schemaComment[0]) == null ? void 0 : _b.description}';`,
|
|
2104
|
+
[],
|
|
2105
|
+
systemUser
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
return scratchPool;
|
|
2109
|
+
}
|
|
2110
|
+
async diffDatabaseToSchema(schema) {
|
|
2111
|
+
const scratchPool = await this.getScratchPool();
|
|
2112
|
+
await this.createDatabaseFromSchema(schema, scratchPool);
|
|
2113
|
+
const originalClient = new Client({
|
|
2114
|
+
database: this.psqlConnectionPool.poolConfig.database,
|
|
2115
|
+
user: this.psqlConnectionPool.poolConfig.user,
|
|
2116
|
+
password: this.psqlConnectionPool.poolConfig.password,
|
|
2117
|
+
host: this.psqlConnectionPool.poolConfig.host,
|
|
2118
|
+
port: this.psqlConnectionPool.poolConfig.port
|
|
2119
|
+
});
|
|
2120
|
+
const scratchClient = new Client({
|
|
2121
|
+
database: this.psqlConnectionPool.poolConfig.database + "_scratch",
|
|
2122
|
+
user: this.psqlConnectionPool.poolConfig.user,
|
|
2123
|
+
password: this.psqlConnectionPool.poolConfig.password,
|
|
2124
|
+
host: this.psqlConnectionPool.poolConfig.host,
|
|
2125
|
+
port: this.psqlConnectionPool.poolConfig.port
|
|
2126
|
+
});
|
|
2127
|
+
const promises = [originalClient.connect(), scratchClient.connect()];
|
|
2128
|
+
await Promise.all(promises);
|
|
2129
|
+
const infoPromises = [pgInfo({ client: originalClient }), pgInfo({ client: scratchClient })];
|
|
2130
|
+
const [info1, info2] = await Promise.all(infoPromises);
|
|
2131
|
+
const diff = getDiff(info1, info2);
|
|
2132
|
+
const endPromises = [originalClient.end(), scratchClient.end()];
|
|
2133
|
+
await Promise.all(endPromises);
|
|
2134
|
+
return diff.join("\n");
|
|
1412
2135
|
}
|
|
1413
2136
|
createNestedSelect(req, schema, item, routeData, userRole, sqlParams) {
|
|
1414
2137
|
if (!item.subquery) return "";
|
|
@@ -1422,8 +2145,7 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
1422
2145
|
)) {
|
|
1423
2146
|
return "'[]'";
|
|
1424
2147
|
}
|
|
1425
|
-
return `COALESCE((
|
|
1426
|
-
SELECT JSON_AGG(JSON_BUILD_OBJECT(
|
|
2148
|
+
return `COALESCE((SELECT JSON_AGG(JSON_BUILD_OBJECT(
|
|
1427
2149
|
${item.subquery.properties.map((nestedItem) => {
|
|
1428
2150
|
if (!this.doesRoleHavePermissionToColumn(req.requesterDetails.role, schema, nestedItem, [
|
|
1429
2151
|
...routeData.joins,
|
|
@@ -1432,7 +2154,7 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
1432
2154
|
return;
|
|
1433
2155
|
}
|
|
1434
2156
|
if (nestedItem.subquery) {
|
|
1435
|
-
return `
|
|
2157
|
+
return `'${nestedItem.name}', ${this.createNestedSelect(
|
|
1436
2158
|
// recursion
|
|
1437
2159
|
req,
|
|
1438
2160
|
schema,
|
|
@@ -1443,7 +2165,7 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
1443
2165
|
)}`;
|
|
1444
2166
|
}
|
|
1445
2167
|
return `'${nestedItem.name}', ${escapeColumnName(nestedItem.selector)}`;
|
|
1446
|
-
}).filter(Boolean).join(",")}
|
|
2168
|
+
}).filter(Boolean).join(", ")}
|
|
1447
2169
|
))
|
|
1448
2170
|
FROM
|
|
1449
2171
|
"${item.subquery.table}"
|
|
@@ -1458,16 +2180,19 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
1458
2180
|
parameterObj[assignment.name] = this.replaceParamKeywords(assignment.value, routeData, req, sqlParams);
|
|
1459
2181
|
});
|
|
1460
2182
|
const query = insertObjectQuery(routeData.table, __spreadValues(__spreadValues({}, req.data), parameterObj));
|
|
1461
|
-
const createdItem = await this.psqlConnectionPool.queryOne(
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
2183
|
+
const createdItem = await this.psqlConnectionPool.queryOne(
|
|
2184
|
+
query,
|
|
2185
|
+
sqlParams,
|
|
2186
|
+
req.requesterDetails
|
|
2187
|
+
);
|
|
2188
|
+
const insertId = createdItem.id;
|
|
2189
|
+
const whereId = {
|
|
2190
|
+
tableName: routeData.table,
|
|
2191
|
+
value: insertId,
|
|
2192
|
+
columnName: "id",
|
|
2193
|
+
operator: "="
|
|
2194
|
+
};
|
|
2195
|
+
const whereData = [whereId];
|
|
1471
2196
|
req.data = { id: insertId };
|
|
1472
2197
|
return this.executeGetRequest(req, __spreadProps(__spreadValues({}, routeData), { where: whereData }), schema);
|
|
1473
2198
|
}
|
|
@@ -1486,7 +2211,9 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
1486
2211
|
let selectStatement = "SELECT \n";
|
|
1487
2212
|
selectStatement += ` ${selectColumns.map((item) => {
|
|
1488
2213
|
if (item.subquery) {
|
|
1489
|
-
return `${this.createNestedSelect(req, schema, item, routeData, userRole, sqlParams)} AS ${
|
|
2214
|
+
return `${this.createNestedSelect(req, schema, item, routeData, userRole, sqlParams)} AS ${escapeColumnName(
|
|
2215
|
+
item.name
|
|
2216
|
+
)}`;
|
|
1490
2217
|
}
|
|
1491
2218
|
return `${escapeColumnName(item.selector)} AS ${escapeColumnName(item.name)}`;
|
|
1492
2219
|
}).join(",\n ")}
|
|
@@ -1519,29 +2246,31 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
1519
2246
|
);
|
|
1520
2247
|
} else if (routeData.type === "PAGED") {
|
|
1521
2248
|
const data = req.data;
|
|
1522
|
-
const
|
|
1523
|
-
`${selectStatement}${sqlStatement}${groupByOrderByStatement} LIMIT
|
|
1524
|
-
|
|
1525
|
-
[
|
|
1526
|
-
...sqlParams,
|
|
1527
|
-
data.perPage || DEFAULT_PAGED_PER_PAGE_NUMBER,
|
|
1528
|
-
(data.page - 1) * data.perPage || DEFAULT_PAGED_PAGE_NUMBER,
|
|
1529
|
-
...sqlParams
|
|
1530
|
-
],
|
|
2249
|
+
const pagePromise = this.psqlConnectionPool.runQuery(
|
|
2250
|
+
`${selectStatement}${sqlStatement}${groupByOrderByStatement}` + SQL`LIMIT ${data.perPage || DEFAULT_PAGED_PER_PAGE_NUMBER} OFFSET ${(data.page - 1) * data.perPage || DEFAULT_PAGED_PAGE_NUMBER};`,
|
|
2251
|
+
sqlParams,
|
|
1531
2252
|
req.requesterDetails
|
|
1532
2253
|
);
|
|
2254
|
+
const totalQuery = `SELECT COUNT(${routeData.groupBy ? `DISTINCT ${routeData.groupBy.tableName}.${routeData.groupBy.columnName}` : "*"}) AS total
|
|
2255
|
+
${sqlStatement};`;
|
|
2256
|
+
const totalPromise = this.psqlConnectionPool.runQuery(
|
|
2257
|
+
totalQuery,
|
|
2258
|
+
sqlParams,
|
|
2259
|
+
req.requesterDetails
|
|
2260
|
+
);
|
|
2261
|
+
const [pageResults, totalResponse] = await Promise.all([pagePromise, totalPromise]);
|
|
1533
2262
|
let total = 0;
|
|
1534
|
-
if (ObjectUtils4.isArrayWithData(
|
|
1535
|
-
total =
|
|
2263
|
+
if (ObjectUtils4.isArrayWithData(totalResponse)) {
|
|
2264
|
+
total = totalResponse[0].total;
|
|
1536
2265
|
}
|
|
1537
|
-
return { data: pageResults
|
|
2266
|
+
return { data: pageResults, total };
|
|
1538
2267
|
} else {
|
|
1539
2268
|
throw new RsError("UNKNOWN_ERROR", "Unknown route type.");
|
|
1540
2269
|
}
|
|
1541
2270
|
}
|
|
1542
2271
|
async executeUpdateRequest(req, routeData, schema) {
|
|
1543
2272
|
const sqlParams = [];
|
|
1544
|
-
const
|
|
2273
|
+
const _a2 = req.body, { id } = _a2, bodyNoId = __objRest(_a2, ["id"]);
|
|
1545
2274
|
const table = schema.database.find((item) => {
|
|
1546
2275
|
return item.name === routeData.table;
|
|
1547
2276
|
});
|
|
@@ -1552,10 +2281,10 @@ ${sqlStatement};`,
|
|
|
1552
2281
|
for (const assignment of routeData.assignments) {
|
|
1553
2282
|
const column = table.columns.find((column2) => column2.name === assignment.name);
|
|
1554
2283
|
if (!column) continue;
|
|
1555
|
-
const
|
|
2284
|
+
const assignmentEscaped = escapeColumnName(assignment.name);
|
|
1556
2285
|
if (SqlUtils.convertDatabaseTypeToTypescript(column.type) === "number")
|
|
1557
|
-
bodyNoId[
|
|
1558
|
-
else bodyNoId[
|
|
2286
|
+
bodyNoId[assignmentEscaped] = Number(assignment.value);
|
|
2287
|
+
else bodyNoId[assignmentEscaped] = assignment.value;
|
|
1559
2288
|
}
|
|
1560
2289
|
const whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
|
|
1561
2290
|
const query = updateObjectQuery(routeData.table, bodyNoId, whereClause);
|
|
@@ -1573,10 +2302,12 @@ ${sqlStatement};`,
|
|
|
1573
2302
|
req.requesterDetails.role,
|
|
1574
2303
|
sqlParams
|
|
1575
2304
|
);
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
2305
|
+
const whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
|
|
2306
|
+
if (whereClause.replace(/\s/g, "") === "") {
|
|
2307
|
+
throw new RsError("DELETE_FORBIDDEN", "Deletes need a where clause");
|
|
2308
|
+
}
|
|
2309
|
+
const deleteStatement = `
|
|
2310
|
+
DELETE FROM "${routeData.table}" ${joinStatement} ${whereClause}`;
|
|
1580
2311
|
await this.psqlConnectionPool.runQuery(deleteStatement, sqlParams, req.requesterDetails);
|
|
1581
2312
|
return true;
|
|
1582
2313
|
}
|
|
@@ -1587,7 +2318,7 @@ ${sqlStatement};`,
|
|
|
1587
2318
|
throw new RsError("UNAUTHORIZED", "You do not have permission to access this table");
|
|
1588
2319
|
if (item.custom) {
|
|
1589
2320
|
const customReplaced = this.replaceParamKeywords(item.custom, routeData, req, sqlParams);
|
|
1590
|
-
joinStatements += ` ${item.type} JOIN ${escapeColumnName(item.table)} ON ${customReplaced}
|
|
2321
|
+
joinStatements += ` ${item.type} JOIN ${escapeColumnName(item.table)}${item.alias ? `AS "${item.alias}"` : ""} ON ${customReplaced}
|
|
1591
2322
|
`;
|
|
1592
2323
|
} else {
|
|
1593
2324
|
joinStatements += ` ${item.type} JOIN ${escapeColumnName(item.table)}${item.alias ? `AS "${item.alias}"` : ""} ON "${baseTable}"."${item.localColumnName}" = ${escapeColumnName(item.alias ? item.alias : item.table)}.${escapeColumnName(
|
|
@@ -1639,38 +2370,37 @@ ${sqlStatement};`,
|
|
|
1639
2370
|
);
|
|
1640
2371
|
let operator = item.operator;
|
|
1641
2372
|
if (operator === "LIKE") {
|
|
1642
|
-
|
|
2373
|
+
item.value = `'%${item.value}%'`;
|
|
1643
2374
|
} else if (operator === "STARTS WITH") {
|
|
1644
2375
|
operator = "LIKE";
|
|
1645
|
-
|
|
2376
|
+
item.value = `'${item.value}%'`;
|
|
1646
2377
|
} else if (operator === "ENDS WITH") {
|
|
1647
2378
|
operator = "LIKE";
|
|
1648
|
-
|
|
2379
|
+
item.value = `'%${item.value}'`;
|
|
1649
2380
|
}
|
|
1650
2381
|
const replacedValue = this.replaceParamKeywords(item.value, routeData, req, sqlParams);
|
|
1651
|
-
|
|
1652
|
-
whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator} ${["IN", "NOT IN"].includes(operator) ? `(${escapedValue})` : escapedValue}
|
|
2382
|
+
whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator.replace("LIKE", "ILIKE")} ${["IN", "NOT IN"].includes(operator) ? `(${replacedValue})` : replacedValue}
|
|
1653
2383
|
`;
|
|
1654
2384
|
});
|
|
1655
2385
|
const data = req.data;
|
|
1656
2386
|
if (routeData.type === "PAGED" && !!(data == null ? void 0 : data.filter)) {
|
|
1657
2387
|
let statement = data.filter.replace(/\$[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
|
|
1658
|
-
var
|
|
2388
|
+
var _a2;
|
|
1659
2389
|
const requestParam = routeData.request.find((item) => {
|
|
1660
2390
|
return item.name === value.replace("$", "");
|
|
1661
2391
|
});
|
|
1662
2392
|
if (!requestParam)
|
|
1663
2393
|
throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
|
|
1664
|
-
return ((
|
|
2394
|
+
return ((_a2 = data[requestParam.name]) == null ? void 0 : _a2.toString()) || "";
|
|
1665
2395
|
});
|
|
1666
2396
|
statement = statement.replace(/#[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
|
|
1667
|
-
var
|
|
2397
|
+
var _a2;
|
|
1668
2398
|
const requestParam = routeData.request.find((item) => {
|
|
1669
2399
|
return item.name === value.replace("#", "");
|
|
1670
2400
|
});
|
|
1671
2401
|
if (!requestParam)
|
|
1672
2402
|
throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
|
|
1673
|
-
return ((
|
|
2403
|
+
return ((_a2 = data[requestParam.name]) == null ? void 0 : _a2.toString()) || "";
|
|
1674
2404
|
});
|
|
1675
2405
|
statement = filterPsqlParser_default.parse(statement);
|
|
1676
2406
|
if (whereClause.startsWith("WHERE")) {
|
|
@@ -1683,82 +2413,259 @@ ${sqlStatement};`,
|
|
|
1683
2413
|
}
|
|
1684
2414
|
return whereClause;
|
|
1685
2415
|
}
|
|
1686
|
-
|
|
2416
|
+
createUpdateTrigger(tableName, notify) {
|
|
2417
|
+
if (!notify) return "";
|
|
2418
|
+
if (notify === "ALL") {
|
|
2419
|
+
return `
|
|
2420
|
+
CREATE OR REPLACE FUNCTION notify_${tableName}_update()
|
|
2421
|
+
RETURNS TRIGGER AS $$
|
|
2422
|
+
DECLARE
|
|
2423
|
+
query_metadata JSON;
|
|
2424
|
+
BEGIN
|
|
2425
|
+
SELECT INTO query_metadata
|
|
2426
|
+
(regexp_match(
|
|
2427
|
+
current_query(),
|
|
2428
|
+
'^--QUERY_METADATA\\(({.*})', 'n'
|
|
2429
|
+
))[1]::json;
|
|
1687
2430
|
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
try {
|
|
1708
|
-
const response = await this.pool.query(formattedQuery, options);
|
|
1709
|
-
if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
|
|
1710
|
-
else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
|
|
1711
|
-
return response.rows[0];
|
|
1712
|
-
} catch (error) {
|
|
1713
|
-
console.error(error, query, options);
|
|
1714
|
-
if (RsError.isRsError(error)) throw error;
|
|
1715
|
-
if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
|
|
1716
|
-
throw new RsError("DUPLICATE", error.message);
|
|
1717
|
-
}
|
|
1718
|
-
throw new RsError("DATABASE_ERROR", `${error.message}`);
|
|
2431
|
+
PERFORM pg_notify(
|
|
2432
|
+
'update',
|
|
2433
|
+
json_build_object(
|
|
2434
|
+
'table', '${tableName}',
|
|
2435
|
+
'queryMetadata', query_metadata,
|
|
2436
|
+
'changedId', NEW.id,
|
|
2437
|
+
'record', NEW,
|
|
2438
|
+
'previousRecord', OLD
|
|
2439
|
+
)::text
|
|
2440
|
+
);
|
|
2441
|
+
RETURN NEW;
|
|
2442
|
+
END;
|
|
2443
|
+
$$ LANGUAGE plpgsql;
|
|
2444
|
+
|
|
2445
|
+
CREATE OR REPLACE TRIGGER ${tableName}_update
|
|
2446
|
+
AFTER UPDATE ON "${tableName}"
|
|
2447
|
+
FOR EACH ROW
|
|
2448
|
+
EXECUTE FUNCTION notify_${tableName}_update();
|
|
2449
|
+
`;
|
|
1719
2450
|
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
2451
|
+
const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
|
|
2452
|
+
const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
|
|
2453
|
+
return `
|
|
2454
|
+
CREATE OR REPLACE FUNCTION notify_${tableName}_update()
|
|
2455
|
+
RETURNS TRIGGER AS $$
|
|
2456
|
+
DECLARE
|
|
2457
|
+
query_metadata JSON;
|
|
2458
|
+
BEGIN
|
|
2459
|
+
SELECT INTO query_metadata
|
|
2460
|
+
(regexp_match(
|
|
2461
|
+
current_query(),
|
|
2462
|
+
'^--QUERY_METADATA\\(({.*})', 'n'
|
|
2463
|
+
))[1]::json;
|
|
2464
|
+
|
|
2465
|
+
PERFORM pg_notify(
|
|
2466
|
+
'update',
|
|
2467
|
+
json_build_object(
|
|
2468
|
+
'table', '${tableName}',
|
|
2469
|
+
'queryMetadata', query_metadata,
|
|
2470
|
+
'changedId', NEW.id,
|
|
2471
|
+
'record', json_build_object(
|
|
2472
|
+
${notifyColumnNewBuildString}
|
|
2473
|
+
),
|
|
2474
|
+
'previousRecord', json_build_object(
|
|
2475
|
+
${notifyColumnOldBuildString}
|
|
2476
|
+
)
|
|
2477
|
+
)::text
|
|
2478
|
+
);
|
|
2479
|
+
RETURN NEW;
|
|
2480
|
+
END;
|
|
2481
|
+
$$ LANGUAGE plpgsql;
|
|
2482
|
+
|
|
2483
|
+
CREATE OR REPLACE TRIGGER ${tableName}_update
|
|
2484
|
+
AFTER UPDATE ON "${tableName}"
|
|
2485
|
+
FOR EACH ROW
|
|
2486
|
+
EXECUTE FUNCTION notify_${tableName}_update();
|
|
2487
|
+
`;
|
|
2488
|
+
}
|
|
2489
|
+
createDeleteTrigger(tableName, notify) {
|
|
2490
|
+
if (!notify) return "";
|
|
2491
|
+
if (notify === "ALL") {
|
|
2492
|
+
return `
|
|
2493
|
+
CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
|
|
2494
|
+
RETURNS TRIGGER AS $$
|
|
2495
|
+
DECLARE
|
|
2496
|
+
query_metadata JSON;
|
|
2497
|
+
BEGIN
|
|
2498
|
+
SELECT INTO query_metadata
|
|
2499
|
+
(regexp_match(
|
|
2500
|
+
current_query(),
|
|
2501
|
+
'^--QUERY_METADATA\\(({.*})', 'n'
|
|
2502
|
+
))[1]::json;
|
|
2503
|
+
|
|
2504
|
+
PERFORM pg_notify(
|
|
2505
|
+
'delete',
|
|
2506
|
+
json_build_object(
|
|
2507
|
+
'table', '${tableName}',
|
|
2508
|
+
'queryMetadata', query_metadata,
|
|
2509
|
+
'deletedId', OLD.id,
|
|
2510
|
+
'previousRecord', OLD
|
|
2511
|
+
)::text
|
|
2512
|
+
);
|
|
2513
|
+
RETURN NEW;
|
|
2514
|
+
END;
|
|
2515
|
+
$$ LANGUAGE plpgsql;
|
|
2516
|
+
|
|
2517
|
+
CREATE OR REPLACE TRIGGER "${tableName}_delete"
|
|
2518
|
+
AFTER DELETE ON "${tableName}"
|
|
2519
|
+
FOR EACH ROW
|
|
2520
|
+
EXECUTE FUNCTION notify_${tableName}_delete();
|
|
2521
|
+
`;
|
|
1736
2522
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
2523
|
+
const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
|
|
2524
|
+
return `
|
|
2525
|
+
CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
|
|
2526
|
+
RETURNS TRIGGER AS $$
|
|
2527
|
+
DECLARE
|
|
2528
|
+
query_metadata JSON;
|
|
2529
|
+
BEGIN
|
|
2530
|
+
SELECT INTO query_metadata
|
|
2531
|
+
(regexp_match(
|
|
2532
|
+
current_query(),
|
|
2533
|
+
'^--QUERY_METADATA\\(({.*})', 'n'
|
|
2534
|
+
))[1]::json;
|
|
2535
|
+
|
|
2536
|
+
PERFORM pg_notify(
|
|
2537
|
+
'delete',
|
|
2538
|
+
json_build_object(
|
|
2539
|
+
'table', '${tableName}',
|
|
2540
|
+
'queryMetadata', query_metadata,
|
|
2541
|
+
'deletedId', OLD.id,
|
|
2542
|
+
'previousRecord', json_build_object(
|
|
2543
|
+
${notifyColumnOldBuildString}
|
|
2544
|
+
)
|
|
2545
|
+
)::text
|
|
2546
|
+
);
|
|
2547
|
+
RETURN NEW;
|
|
2548
|
+
END;
|
|
2549
|
+
$$ LANGUAGE plpgsql;
|
|
2550
|
+
|
|
2551
|
+
CREATE OR REPLACE TRIGGER "${tableName}_delete"
|
|
2552
|
+
AFTER DELETE ON "${tableName}"
|
|
2553
|
+
FOR EACH ROW
|
|
2554
|
+
EXECUTE FUNCTION notify_${tableName}_delete();
|
|
2555
|
+
`;
|
|
2556
|
+
}
|
|
2557
|
+
createInsertTriggers(tableName, notify) {
|
|
2558
|
+
if (!notify) return "";
|
|
2559
|
+
if (notify === "ALL") {
|
|
2560
|
+
return `
|
|
2561
|
+
CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
|
|
2562
|
+
RETURNS TRIGGER AS $$
|
|
2563
|
+
DECLARE
|
|
2564
|
+
query_metadata JSON;
|
|
2565
|
+
BEGIN
|
|
2566
|
+
SELECT INTO query_metadata
|
|
2567
|
+
(regexp_match(
|
|
2568
|
+
current_query(),
|
|
2569
|
+
'^--QUERY_METADATA\\(({.*})', 'n'
|
|
2570
|
+
))[1]::json;
|
|
2571
|
+
|
|
2572
|
+
PERFORM pg_notify(
|
|
2573
|
+
'insert',
|
|
2574
|
+
json_build_object(
|
|
2575
|
+
'table', '${tableName}',
|
|
2576
|
+
'queryMetadata', query_metadata,
|
|
2577
|
+
'insertedId', NEW.id,
|
|
2578
|
+
'record', NEW
|
|
2579
|
+
)::text
|
|
2580
|
+
);
|
|
2581
|
+
|
|
2582
|
+
RETURN NEW;
|
|
2583
|
+
END;
|
|
2584
|
+
$$ LANGUAGE plpgsql;
|
|
2585
|
+
|
|
2586
|
+
CREATE OR REPLACE TRIGGER "${tableName}_insert"
|
|
2587
|
+
AFTER INSERT ON "${tableName}"
|
|
2588
|
+
FOR EACH ROW
|
|
2589
|
+
EXECUTE FUNCTION notify_${tableName}_insert();
|
|
2590
|
+
`;
|
|
1750
2591
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
2592
|
+
const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
|
|
2593
|
+
return `
|
|
2594
|
+
CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
|
|
2595
|
+
RETURNS TRIGGER AS $$
|
|
2596
|
+
DECLARE
|
|
2597
|
+
query_metadata JSON;
|
|
2598
|
+
BEGIN
|
|
2599
|
+
SELECT INTO query_metadata
|
|
2600
|
+
(regexp_match(
|
|
2601
|
+
current_query(),
|
|
2602
|
+
'^--QUERY_METADATA\\(({.*})', 'n'
|
|
2603
|
+
))[1]::json;
|
|
2604
|
+
|
|
2605
|
+
PERFORM pg_notify(
|
|
2606
|
+
'insert',
|
|
2607
|
+
json_build_object(
|
|
2608
|
+
'table', '${tableName}',
|
|
2609
|
+
'queryMetadata', query_metadata,
|
|
2610
|
+
'insertedId', NEW.id,
|
|
2611
|
+
'record', json_build_object(
|
|
2612
|
+
${notifyColumnNewBuildString}
|
|
2613
|
+
)
|
|
2614
|
+
)::text
|
|
2615
|
+
);
|
|
2616
|
+
|
|
2617
|
+
RETURN NEW;
|
|
2618
|
+
END;
|
|
2619
|
+
$$ LANGUAGE plpgsql;
|
|
2620
|
+
|
|
2621
|
+
CREATE OR REPLACE TRIGGER "${tableName}_insert"
|
|
2622
|
+
AFTER INSERT ON "${tableName}"
|
|
2623
|
+
FOR EACH ROW
|
|
2624
|
+
EXECUTE FUNCTION notify_${tableName}_insert();
|
|
2625
|
+
`;
|
|
2626
|
+
}
|
|
2627
|
+
schemaToPsqlType(column) {
|
|
2628
|
+
if (column.hasAutoIncrement) return "BIGSERIAL";
|
|
2629
|
+
if (column.type === "ENUM") return `TEXT`;
|
|
2630
|
+
if (column.type === "DATETIME") return "TIMESTAMPTZ";
|
|
2631
|
+
if (column.type === "MEDIUMINT") return "INT";
|
|
2632
|
+
return column.type;
|
|
2633
|
+
}
|
|
2634
|
+
};
|
|
2635
|
+
|
|
2636
|
+
// src/restura/utils/TempCache.ts
|
|
2637
|
+
import fs3 from "fs";
|
|
2638
|
+
import path3 from "path";
|
|
2639
|
+
import { DateUtils } from "@redskytech/core-utils";
|
|
2640
|
+
import { FileUtils as FileUtils2 } from "@restura/internal";
|
|
2641
|
+
import Bluebird3 from "bluebird";
|
|
2642
|
+
import * as os2 from "os";
|
|
2643
|
+
var TempCache = class {
|
|
2644
|
+
constructor(location) {
|
|
2645
|
+
this.maxDurationDays = 7;
|
|
2646
|
+
this.location = location || os2.tmpdir();
|
|
2647
|
+
FileUtils2.ensureDir(this.location).catch((e) => {
|
|
2648
|
+
throw e;
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
async cleanup() {
|
|
2652
|
+
const fileList = await fs3.promises.readdir(this.location);
|
|
2653
|
+
await Bluebird3.map(
|
|
2654
|
+
fileList,
|
|
2655
|
+
async (file) => {
|
|
2656
|
+
const fullFilePath = path3.join(this.location, file);
|
|
2657
|
+
const fileStats = await fs3.promises.stat(fullFilePath);
|
|
2658
|
+
if (DateUtils.daysBetweenStartAndEndDates(new Date(fileStats.mtimeMs), /* @__PURE__ */ new Date()) > this.maxDurationDays) {
|
|
2659
|
+
logger.info(`Deleting old temp file: ${file}`);
|
|
2660
|
+
await fs3.promises.unlink(fullFilePath);
|
|
2661
|
+
}
|
|
2662
|
+
},
|
|
2663
|
+
{ concurrency: 10 }
|
|
2664
|
+
);
|
|
1757
2665
|
}
|
|
1758
2666
|
};
|
|
1759
2667
|
|
|
1760
2668
|
// src/restura/restura.ts
|
|
1761
|
-
var { types } = pg2;
|
|
1762
2669
|
var ResturaEngine = class {
|
|
1763
2670
|
constructor() {
|
|
1764
2671
|
this.publicEndpoints = {
|
|
@@ -1777,9 +2684,10 @@ var ResturaEngine = class {
|
|
|
1777
2684
|
*/
|
|
1778
2685
|
async init(app, authenticationHandler, psqlConnectionPool) {
|
|
1779
2686
|
this.resturaConfig = config2.validate("restura", resturaConfigSchema);
|
|
2687
|
+
this.multerCommonUpload = getMulterUpload(this.resturaConfig.fileTempCachePath);
|
|
2688
|
+
new TempCache(this.resturaConfig.fileTempCachePath);
|
|
1780
2689
|
this.psqlConnectionPool = psqlConnectionPool;
|
|
1781
|
-
this.psqlEngine = new PsqlEngine(this.psqlConnectionPool);
|
|
1782
|
-
setupPgReturnTypes();
|
|
2690
|
+
this.psqlEngine = new PsqlEngine(this.psqlConnectionPool, true);
|
|
1783
2691
|
await customApiFactory_default.loadApiFiles(this.resturaConfig.customApiFolderPath);
|
|
1784
2692
|
this.authenticationHandler = authenticationHandler;
|
|
1785
2693
|
app.use(compression());
|
|
@@ -1843,10 +2751,7 @@ var ResturaEngine = class {
|
|
|
1843
2751
|
* @returns A promise that resolves when the API has been successfully generated and written to the output file.
|
|
1844
2752
|
*/
|
|
1845
2753
|
async generateApiFromSchema(outputFile, providedSchema) {
|
|
1846
|
-
|
|
1847
|
-
outputFile,
|
|
1848
|
-
await apiGenerator(providedSchema, await this.generateHashForSchema(providedSchema))
|
|
1849
|
-
);
|
|
2754
|
+
fs4.writeFileSync(outputFile, await apiGenerator(providedSchema));
|
|
1850
2755
|
}
|
|
1851
2756
|
/**
|
|
1852
2757
|
* Generates a model from the provided schema and writes it to the specified output file.
|
|
@@ -1856,10 +2761,15 @@ var ResturaEngine = class {
|
|
|
1856
2761
|
* @returns A promise that resolves when the model has been successfully written to the output file.
|
|
1857
2762
|
*/
|
|
1858
2763
|
async generateModelFromSchema(outputFile, providedSchema) {
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
2764
|
+
fs4.writeFileSync(outputFile, await modelGenerator(providedSchema));
|
|
2765
|
+
}
|
|
2766
|
+
/**
|
|
2767
|
+
* Generates the ambient module declaration for Restura global types and writes it to the specified output file.
|
|
2768
|
+
* These types are used sometimes in the CustomTypes
|
|
2769
|
+
* @param outputFile
|
|
2770
|
+
*/
|
|
2771
|
+
generateResturaGlobalTypes(outputFile) {
|
|
2772
|
+
fs4.writeFileSync(outputFile, resturaGlobalTypesGenerator());
|
|
1863
2773
|
}
|
|
1864
2774
|
/**
|
|
1865
2775
|
* Retrieves the latest file system schema for Restura.
|
|
@@ -1868,11 +2778,11 @@ var ResturaEngine = class {
|
|
|
1868
2778
|
* @throws {Error} If the schema file is missing or the schema is not valid.
|
|
1869
2779
|
*/
|
|
1870
2780
|
async getLatestFileSystemSchema() {
|
|
1871
|
-
if (!
|
|
2781
|
+
if (!fs4.existsSync(this.resturaConfig.schemaFilePath)) {
|
|
1872
2782
|
logger.error(`Missing restura schema file, expected path: ${this.resturaConfig.schemaFilePath}`);
|
|
1873
2783
|
throw new Error("Missing restura schema file");
|
|
1874
2784
|
}
|
|
1875
|
-
const schemaFileData =
|
|
2785
|
+
const schemaFileData = fs4.readFileSync(this.resturaConfig.schemaFilePath, { encoding: "utf8" });
|
|
1876
2786
|
const schema = ObjectUtils5.safeParse(schemaFileData);
|
|
1877
2787
|
const isValid = await isSchemaValid(schema);
|
|
1878
2788
|
if (!isValid) {
|
|
@@ -1881,28 +2791,6 @@ var ResturaEngine = class {
|
|
|
1881
2791
|
}
|
|
1882
2792
|
return schema;
|
|
1883
2793
|
}
|
|
1884
|
-
/**
|
|
1885
|
-
* Asynchronously generates and retrieves hashes for the provided schema and related generated files.
|
|
1886
|
-
*
|
|
1887
|
-
* @param providedSchema - The schema for which hashes need to be generated.
|
|
1888
|
-
* @returns A promise that resolves to an object containing:
|
|
1889
|
-
* - `schemaHash`: The hash of the provided schema.
|
|
1890
|
-
* - `apiCreatedSchemaHash`: The hash extracted from the generated `api.d.ts` file.
|
|
1891
|
-
* - `modelCreatedSchemaHash`: The hash extracted from the generated `models.d.ts` file.
|
|
1892
|
-
*/
|
|
1893
|
-
async getHashes(providedSchema) {
|
|
1894
|
-
var _a, _b, _c, _d;
|
|
1895
|
-
const schemaHash = await this.generateHashForSchema(providedSchema);
|
|
1896
|
-
const apiFile = fs3.readFileSync(path3.join(this.resturaConfig.generatedTypesPath, "api.d.ts"));
|
|
1897
|
-
const apiCreatedSchemaHash = (_b = (_a = apiFile.toString().match(/\((.*)\)/)) == null ? void 0 : _a[1]) != null ? _b : "";
|
|
1898
|
-
const modelFile = fs3.readFileSync(path3.join(this.resturaConfig.generatedTypesPath, "models.d.ts"));
|
|
1899
|
-
const modelCreatedSchemaHash = (_d = (_c = modelFile.toString().match(/\((.*)\)/)) == null ? void 0 : _c[1]) != null ? _d : "";
|
|
1900
|
-
return {
|
|
1901
|
-
schemaHash,
|
|
1902
|
-
apiCreatedSchemaHash,
|
|
1903
|
-
modelCreatedSchemaHash
|
|
1904
|
-
};
|
|
1905
|
-
}
|
|
1906
2794
|
async reloadEndpoints() {
|
|
1907
2795
|
this.schema = await this.getLatestFileSystemSchema();
|
|
1908
2796
|
this.customTypeValidation = customTypeValidationGenerator(this.schema);
|
|
@@ -1931,30 +2819,10 @@ var ResturaEngine = class {
|
|
|
1931
2819
|
logger.info(`Restura loaded (${routeCount}) endpoint${routeCount > 1 ? "s" : ""}`);
|
|
1932
2820
|
}
|
|
1933
2821
|
async validateGeneratedTypesFolder() {
|
|
1934
|
-
if (!
|
|
1935
|
-
|
|
1936
|
-
}
|
|
1937
|
-
const hasApiFile = fs3.existsSync(path3.join(this.resturaConfig.generatedTypesPath, "api.d.ts"));
|
|
1938
|
-
const hasModelsFile = fs3.existsSync(path3.join(this.resturaConfig.generatedTypesPath, "models.d.ts"));
|
|
1939
|
-
if (!hasApiFile) {
|
|
1940
|
-
await this.generateApiFromSchema(path3.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
|
|
1941
|
-
}
|
|
1942
|
-
if (!hasModelsFile) {
|
|
1943
|
-
await this.generateModelFromSchema(
|
|
1944
|
-
path3.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
|
|
1945
|
-
this.schema
|
|
1946
|
-
);
|
|
1947
|
-
}
|
|
1948
|
-
const hashes = await this.getHashes(this.schema);
|
|
1949
|
-
if (hashes.schemaHash !== hashes.apiCreatedSchemaHash) {
|
|
1950
|
-
await this.generateApiFromSchema(path3.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
|
|
1951
|
-
}
|
|
1952
|
-
if (hashes.schemaHash !== hashes.modelCreatedSchemaHash) {
|
|
1953
|
-
await this.generateModelFromSchema(
|
|
1954
|
-
path3.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
|
|
1955
|
-
this.schema
|
|
1956
|
-
);
|
|
2822
|
+
if (!fs4.existsSync(this.resturaConfig.generatedTypesPath)) {
|
|
2823
|
+
fs4.mkdirSync(this.resturaConfig.generatedTypesPath, { recursive: true });
|
|
1957
2824
|
}
|
|
2825
|
+
this.updateTypes();
|
|
1958
2826
|
}
|
|
1959
2827
|
resturaAuthentication(req, res, next) {
|
|
1960
2828
|
if (req.headers["x-auth-token"] !== this.resturaConfig.authToken) res.status(401).send("Unauthorized");
|
|
@@ -1962,7 +2830,7 @@ var ResturaEngine = class {
|
|
|
1962
2830
|
}
|
|
1963
2831
|
async previewCreateSchema(req, res) {
|
|
1964
2832
|
try {
|
|
1965
|
-
const schemaDiff =
|
|
2833
|
+
const schemaDiff = await compareSchema_default.diffSchema(req.data, this.schema, this.psqlEngine);
|
|
1966
2834
|
res.send({ data: schemaDiff });
|
|
1967
2835
|
} catch (err) {
|
|
1968
2836
|
res.status(400).send(err);
|
|
@@ -1970,7 +2838,7 @@ var ResturaEngine = class {
|
|
|
1970
2838
|
}
|
|
1971
2839
|
async updateSchema(req, res) {
|
|
1972
2840
|
try {
|
|
1973
|
-
this.schema = req.data;
|
|
2841
|
+
this.schema = sortObjectKeysAlphabetically(req.data);
|
|
1974
2842
|
await this.storeFileSystemSchema();
|
|
1975
2843
|
await this.reloadEndpoints();
|
|
1976
2844
|
await this.updateTypes();
|
|
@@ -1981,11 +2849,12 @@ var ResturaEngine = class {
|
|
|
1981
2849
|
}
|
|
1982
2850
|
}
|
|
1983
2851
|
async updateTypes() {
|
|
1984
|
-
await this.generateApiFromSchema(
|
|
2852
|
+
await this.generateApiFromSchema(path4.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
|
|
1985
2853
|
await this.generateModelFromSchema(
|
|
1986
|
-
|
|
2854
|
+
path4.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
|
|
1987
2855
|
this.schema
|
|
1988
2856
|
);
|
|
2857
|
+
this.generateResturaGlobalTypes(path4.join(this.resturaConfig.generatedTypesPath, "restura.d.ts"));
|
|
1989
2858
|
}
|
|
1990
2859
|
async getSchema(req, res) {
|
|
1991
2860
|
res.send({ data: this.schema });
|
|
@@ -1993,19 +2862,38 @@ var ResturaEngine = class {
|
|
|
1993
2862
|
async getSchemaAndTypes(req, res) {
|
|
1994
2863
|
try {
|
|
1995
2864
|
const schema = await this.getLatestFileSystemSchema();
|
|
1996
|
-
const
|
|
1997
|
-
const
|
|
1998
|
-
const modelsText = await modelGenerator(schema, schemaHash);
|
|
2865
|
+
const apiText = await apiGenerator(schema);
|
|
2866
|
+
const modelsText = await modelGenerator(schema);
|
|
1999
2867
|
res.send({ schema, api: apiText, models: modelsText });
|
|
2000
2868
|
} catch (err) {
|
|
2001
2869
|
res.status(400).send({ error: err });
|
|
2002
2870
|
}
|
|
2003
2871
|
}
|
|
2872
|
+
async getMulterFilesIfAny(req, res, routeData) {
|
|
2873
|
+
var _a2;
|
|
2874
|
+
if (!((_a2 = req.header("content-type")) == null ? void 0 : _a2.includes("multipart/form-data"))) return;
|
|
2875
|
+
if (!this.isCustomRoute(routeData)) return;
|
|
2876
|
+
if (!routeData.fileUploadType) {
|
|
2877
|
+
throw new RsError("BAD_REQUEST", "File upload type not defined for route");
|
|
2878
|
+
}
|
|
2879
|
+
const multerFileUploadFunction = routeData.fileUploadType === "MULTIPLE" ? this.multerCommonUpload.array("files") : this.multerCommonUpload.single("file");
|
|
2880
|
+
return new Promise((resolve2, reject) => {
|
|
2881
|
+
multerFileUploadFunction(req, res, (err) => {
|
|
2882
|
+
if (err) {
|
|
2883
|
+
logger.warn("Multer error: " + err);
|
|
2884
|
+
reject(err);
|
|
2885
|
+
}
|
|
2886
|
+
if (req.body["data"]) req.body = JSON.parse(req.body["data"]);
|
|
2887
|
+
resolve2();
|
|
2888
|
+
});
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2004
2891
|
async executeRouteLogic(req, res, next) {
|
|
2005
2892
|
try {
|
|
2006
2893
|
const routeData = this.getRouteData(req.method, req.baseUrl, req.path);
|
|
2007
2894
|
this.validateAuthorization(req, routeData);
|
|
2008
|
-
|
|
2895
|
+
await this.getMulterFilesIfAny(req, res, routeData);
|
|
2896
|
+
requestValidator(req, routeData, this.customTypeValidation);
|
|
2009
2897
|
if (this.isCustomRoute(routeData)) {
|
|
2010
2898
|
await this.runCustomRouteLogic(req, res, routeData);
|
|
2011
2899
|
return;
|
|
@@ -2041,22 +2929,10 @@ var ResturaEngine = class {
|
|
|
2041
2929
|
return acc + StringUtils3.capitalizeFirst(cur);
|
|
2042
2930
|
}, "")}`;
|
|
2043
2931
|
const customFunction = customApi[functionName];
|
|
2044
|
-
if (!customFunction)
|
|
2932
|
+
if (!customFunction)
|
|
2933
|
+
throw new RsError("NOT_FOUND", `API path ${routeData.path} not implemented ${functionName}`);
|
|
2045
2934
|
await customFunction(req, res, routeData);
|
|
2046
2935
|
}
|
|
2047
|
-
async generateHashForSchema(providedSchema) {
|
|
2048
|
-
const schemaPrettyStr = await prettier3.format(JSON.stringify(providedSchema), __spreadValues({
|
|
2049
|
-
parser: "json"
|
|
2050
|
-
}, {
|
|
2051
|
-
trailingComma: "none",
|
|
2052
|
-
tabWidth: 4,
|
|
2053
|
-
useTabs: true,
|
|
2054
|
-
endOfLine: "lf",
|
|
2055
|
-
printWidth: 120,
|
|
2056
|
-
singleQuote: true
|
|
2057
|
-
}));
|
|
2058
|
-
return createHash("sha256").update(schemaPrettyStr).digest("hex");
|
|
2059
|
-
}
|
|
2060
2936
|
async storeFileSystemSchema() {
|
|
2061
2937
|
const schemaPrettyStr = await prettier3.format(JSON.stringify(this.schema), __spreadValues({
|
|
2062
2938
|
parser: "json"
|
|
@@ -2068,7 +2944,7 @@ var ResturaEngine = class {
|
|
|
2068
2944
|
printWidth: 120,
|
|
2069
2945
|
singleQuote: true
|
|
2070
2946
|
}));
|
|
2071
|
-
|
|
2947
|
+
fs4.writeFileSync(this.resturaConfig.schemaFilePath, schemaPrettyStr);
|
|
2072
2948
|
}
|
|
2073
2949
|
resetPublicEndpoints() {
|
|
2074
2950
|
this.publicEndpoints = {
|
|
@@ -2085,13 +2961,13 @@ var ResturaEngine = class {
|
|
|
2085
2961
|
if (!routeData.roles.includes(role))
|
|
2086
2962
|
throw new RsError("UNAUTHORIZED", "Not authorized to access this endpoint");
|
|
2087
2963
|
}
|
|
2088
|
-
getRouteData(method, baseUrl,
|
|
2964
|
+
getRouteData(method, baseUrl, path5) {
|
|
2089
2965
|
const endpoint = this.schema.endpoints.find((item) => {
|
|
2090
2966
|
return item.baseUrl === baseUrl;
|
|
2091
2967
|
});
|
|
2092
2968
|
if (!endpoint) throw new RsError("NOT_FOUND", "Route not found");
|
|
2093
2969
|
const route = endpoint.routes.find((item) => {
|
|
2094
|
-
return item.method === method && item.path ===
|
|
2970
|
+
return item.method === method && item.path === path5;
|
|
2095
2971
|
});
|
|
2096
2972
|
if (!route) throw new RsError("NOT_FOUND", "Route not found");
|
|
2097
2973
|
return route;
|
|
@@ -2112,6 +2988,9 @@ __decorateClass([
|
|
|
2112
2988
|
__decorateClass([
|
|
2113
2989
|
boundMethod
|
|
2114
2990
|
], ResturaEngine.prototype, "getSchemaAndTypes", 1);
|
|
2991
|
+
__decorateClass([
|
|
2992
|
+
boundMethod
|
|
2993
|
+
], ResturaEngine.prototype, "getMulterFilesIfAny", 1);
|
|
2115
2994
|
__decorateClass([
|
|
2116
2995
|
boundMethod
|
|
2117
2996
|
], ResturaEngine.prototype, "executeRouteLogic", 1);
|
|
@@ -2121,24 +3000,54 @@ __decorateClass([
|
|
|
2121
3000
|
__decorateClass([
|
|
2122
3001
|
boundMethod
|
|
2123
3002
|
], ResturaEngine.prototype, "runCustomRouteLogic", 1);
|
|
2124
|
-
var setupPgReturnTypes = () => {
|
|
2125
|
-
const TIMESTAMPTZ_OID = 1184;
|
|
2126
|
-
types.setTypeParser(TIMESTAMPTZ_OID, (val) => {
|
|
2127
|
-
return val === null ? null : new Date(val).toISOString();
|
|
2128
|
-
});
|
|
2129
|
-
const BIGINT_OID = 20;
|
|
2130
|
-
types.setTypeParser(BIGINT_OID, (val) => {
|
|
2131
|
-
return val === null ? null : Number(val);
|
|
2132
|
-
});
|
|
2133
|
-
};
|
|
2134
|
-
setupPgReturnTypes();
|
|
2135
3003
|
var restura = new ResturaEngine();
|
|
3004
|
+
|
|
3005
|
+
// src/restura/sql/PsqlTransaction.ts
|
|
3006
|
+
import pg3 from "pg";
|
|
3007
|
+
var { Client: Client2 } = pg3;
|
|
3008
|
+
var PsqlTransaction = class extends PsqlConnection {
|
|
3009
|
+
constructor(clientConfig, instanceId) {
|
|
3010
|
+
super(instanceId);
|
|
3011
|
+
this.clientConfig = clientConfig;
|
|
3012
|
+
this.client = new Client2(clientConfig);
|
|
3013
|
+
this.connectPromise = this.client.connect();
|
|
3014
|
+
this.beginTransactionPromise = this.beginTransaction();
|
|
3015
|
+
}
|
|
3016
|
+
async close() {
|
|
3017
|
+
if (this.client) {
|
|
3018
|
+
await this.client.end();
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
async beginTransaction() {
|
|
3022
|
+
await this.connectPromise;
|
|
3023
|
+
return this.client.query("BEGIN");
|
|
3024
|
+
}
|
|
3025
|
+
async rollback() {
|
|
3026
|
+
return this.query("ROLLBACK");
|
|
3027
|
+
}
|
|
3028
|
+
async commit() {
|
|
3029
|
+
return this.query("COMMIT");
|
|
3030
|
+
}
|
|
3031
|
+
async release() {
|
|
3032
|
+
return this.client.end();
|
|
3033
|
+
}
|
|
3034
|
+
async query(query, values) {
|
|
3035
|
+
await this.connectPromise;
|
|
3036
|
+
await this.beginTransactionPromise;
|
|
3037
|
+
return this.client.query(query, values);
|
|
3038
|
+
}
|
|
3039
|
+
};
|
|
2136
3040
|
export {
|
|
2137
3041
|
HtmlStatusCodes,
|
|
3042
|
+
PsqlConnection,
|
|
3043
|
+
PsqlEngine,
|
|
2138
3044
|
PsqlPool,
|
|
3045
|
+
PsqlTransaction,
|
|
2139
3046
|
RsError,
|
|
2140
3047
|
SQL,
|
|
2141
3048
|
escapeColumnName,
|
|
3049
|
+
eventManager_default as eventManager,
|
|
3050
|
+
filterPsqlParser_default as filterPsqlParser,
|
|
2142
3051
|
insertObjectQuery,
|
|
2143
3052
|
isValueNumber2 as isValueNumber,
|
|
2144
3053
|
logger,
|