@restura/core 0.1.0-alpha.9 → 0.1.1
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 +1394 -252
- package/dist/index.d.ts +1394 -252
- package/dist/index.js +1345 -451
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1332 -443
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -2
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}${column.isNullable ? "?" : ""}: ${SqlUtils.convertDatabaseTypeToTypescript(column.type, column.value)};
|
|
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,33 @@ 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";
|
|
1204
1541
|
|
|
1205
1542
|
// src/restura/sql/PsqlUtils.ts
|
|
1206
1543
|
import format2 from "pg-format";
|
|
@@ -1210,16 +1547,33 @@ function escapeColumnName(columnName) {
|
|
|
1210
1547
|
}
|
|
1211
1548
|
function questionMarksToOrderedParams(query) {
|
|
1212
1549
|
let count = 1;
|
|
1213
|
-
|
|
1550
|
+
let inSingleQuote = false;
|
|
1551
|
+
let inDoubleQuote = false;
|
|
1552
|
+
return query.replace(/('|"|\?)/g, (char) => {
|
|
1553
|
+
if (char === "'") {
|
|
1554
|
+
inSingleQuote = !inSingleQuote && !inDoubleQuote;
|
|
1555
|
+
return char;
|
|
1556
|
+
}
|
|
1557
|
+
if (char === '"') {
|
|
1558
|
+
inDoubleQuote = !inDoubleQuote && !inSingleQuote;
|
|
1559
|
+
return char;
|
|
1560
|
+
}
|
|
1561
|
+
if (char === "?" && !inSingleQuote && !inDoubleQuote) {
|
|
1562
|
+
return `$${count++}`;
|
|
1563
|
+
}
|
|
1564
|
+
return char;
|
|
1565
|
+
});
|
|
1214
1566
|
}
|
|
1215
1567
|
function insertObjectQuery(table, obj) {
|
|
1216
1568
|
const keys = Object.keys(obj);
|
|
1217
1569
|
const params = Object.values(obj);
|
|
1218
1570
|
const columns = keys.map((column) => escapeColumnName(column)).join(", ");
|
|
1219
1571
|
const values = params.map((value) => SQL`${value}`).join(", ");
|
|
1220
|
-
|
|
1572
|
+
let query = `
|
|
1573
|
+
INSERT INTO "${table}" (${columns})
|
|
1221
1574
|
VALUES (${values})
|
|
1222
1575
|
RETURNING *`;
|
|
1576
|
+
query = query.replace(/'(\?)'/, "?");
|
|
1223
1577
|
return query;
|
|
1224
1578
|
}
|
|
1225
1579
|
function updateObjectQuery(table, obj, whereStatement) {
|
|
@@ -1227,25 +1581,113 @@ function updateObjectQuery(table, obj, whereStatement) {
|
|
|
1227
1581
|
for (const i in obj) {
|
|
1228
1582
|
setArray.push(`${escapeColumnName(i)} = ` + SQL`${obj[i]}`);
|
|
1229
1583
|
}
|
|
1230
|
-
return `
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1584
|
+
return `
|
|
1585
|
+
UPDATE ${escapeColumnName(table)}
|
|
1586
|
+
SET ${setArray.join(", ")} ${whereStatement}
|
|
1587
|
+
RETURNING *`;
|
|
1588
|
+
}
|
|
1589
|
+
function isValueNumber2(value) {
|
|
1590
|
+
return !isNaN(Number(value));
|
|
1591
|
+
}
|
|
1592
|
+
function SQL(strings, ...values) {
|
|
1593
|
+
let query = strings[0];
|
|
1594
|
+
values.forEach((value, index) => {
|
|
1595
|
+
if (typeof value === "boolean") {
|
|
1596
|
+
query += value;
|
|
1597
|
+
} else if (typeof value === "number") {
|
|
1598
|
+
query += value;
|
|
1599
|
+
} else if (Array.isArray(value)) {
|
|
1600
|
+
query += format2.literal(JSON.stringify(value)) + "::jsonb";
|
|
1601
|
+
} else {
|
|
1602
|
+
query += format2.literal(value);
|
|
1603
|
+
}
|
|
1604
|
+
query += strings[index + 1];
|
|
1605
|
+
});
|
|
1606
|
+
return query;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// src/restura/sql/PsqlConnection.ts
|
|
1610
|
+
var PsqlConnection = class {
|
|
1611
|
+
constructor(instanceId) {
|
|
1612
|
+
this.instanceId = instanceId || crypto.randomUUID();
|
|
1613
|
+
}
|
|
1614
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1615
|
+
async queryOne(query, options, requesterDetails) {
|
|
1616
|
+
const formattedQuery = questionMarksToOrderedParams(query);
|
|
1617
|
+
const meta = __spreadValues({ connectionInstanceId: this.instanceId }, requesterDetails);
|
|
1618
|
+
this.logSqlStatement(formattedQuery, options, meta);
|
|
1619
|
+
const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
|
|
1620
|
+
`;
|
|
1621
|
+
try {
|
|
1622
|
+
const response = await this.query(queryMetadata + formattedQuery, options);
|
|
1623
|
+
if (response.rows.length === 0) throw new RsError("NOT_FOUND", "No results found");
|
|
1624
|
+
else if (response.rows.length > 1) throw new RsError("DUPLICATE", "More than one result found");
|
|
1625
|
+
return response.rows[0];
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
if (RsError.isRsError(error)) throw error;
|
|
1628
|
+
if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
|
|
1629
|
+
throw new RsError("DUPLICATE", error.message);
|
|
1630
|
+
}
|
|
1631
|
+
throw new RsError("DATABASE_ERROR", `${error.message}`);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1635
|
+
async runQuery(query, options, requesterDetails) {
|
|
1636
|
+
const formattedQuery = questionMarksToOrderedParams(query);
|
|
1637
|
+
const meta = __spreadValues({ connectionInstanceId: this.instanceId }, requesterDetails);
|
|
1638
|
+
this.logSqlStatement(formattedQuery, options, meta);
|
|
1639
|
+
const queryMetadata = `--QUERY_METADATA(${JSON.stringify(meta)})
|
|
1640
|
+
`;
|
|
1641
|
+
try {
|
|
1642
|
+
const response = await this.query(queryMetadata + formattedQuery, options);
|
|
1643
|
+
return response.rows;
|
|
1644
|
+
} catch (error) {
|
|
1645
|
+
if ((error == null ? void 0 : error.routine) === "_bt_check_unique") {
|
|
1646
|
+
throw new RsError("DUPLICATE", error.message);
|
|
1647
|
+
}
|
|
1648
|
+
throw new RsError("DATABASE_ERROR", `${error.message}`);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
logSqlStatement(query, options, queryMetadata, prefix = "") {
|
|
1652
|
+
if (logger.level !== "silly") return;
|
|
1653
|
+
let sqlStatement = "";
|
|
1654
|
+
if (options.length === 0) {
|
|
1655
|
+
sqlStatement = query;
|
|
1242
1656
|
} else {
|
|
1243
|
-
|
|
1657
|
+
let stringIndex = 0;
|
|
1658
|
+
sqlStatement = query.replace(/\$\d+/g, () => {
|
|
1659
|
+
const value = options[stringIndex++];
|
|
1660
|
+
if (typeof value === "number") return value.toString();
|
|
1661
|
+
return format3.literal(value);
|
|
1662
|
+
});
|
|
1244
1663
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1664
|
+
let initiator = "Anonymous";
|
|
1665
|
+
if ("userId" in queryMetadata && queryMetadata.userId)
|
|
1666
|
+
initiator = `User Id (${queryMetadata.userId.toString()})`;
|
|
1667
|
+
if ("isSystemUser" in queryMetadata && queryMetadata.isSystemUser) initiator = "SYSTEM";
|
|
1668
|
+
logger.silly(`${prefix}query by ${initiator}, Query ->
|
|
1669
|
+
${sqlStatement}`);
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
|
|
1673
|
+
// src/restura/sql/PsqlPool.ts
|
|
1674
|
+
var { Pool } = pg;
|
|
1675
|
+
var PsqlPool = class extends PsqlConnection {
|
|
1676
|
+
constructor(poolConfig) {
|
|
1677
|
+
super();
|
|
1678
|
+
this.poolConfig = poolConfig;
|
|
1679
|
+
this.pool = new Pool(poolConfig);
|
|
1680
|
+
this.queryOne("SELECT NOW();", [], { isSystemUser: true, role: "", host: "localhost", ipAddress: "" }).then(() => {
|
|
1681
|
+
logger.info("Connected to PostgreSQL database");
|
|
1682
|
+
}).catch((error) => {
|
|
1683
|
+
logger.error("Error connecting to database", error);
|
|
1684
|
+
process.exit(1);
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
async query(query, values) {
|
|
1688
|
+
return this.pool.query(query, values);
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1249
1691
|
|
|
1250
1692
|
// src/restura/sql/SqlEngine.ts
|
|
1251
1693
|
import { ObjectUtils as ObjectUtils3 } from "@redskytech/core-utils";
|
|
@@ -1313,11 +1755,11 @@ var SqlEngine = class {
|
|
|
1313
1755
|
return returnValue;
|
|
1314
1756
|
}
|
|
1315
1757
|
replaceLocalParamKeywords(value, routeData, req, sqlParams) {
|
|
1316
|
-
var
|
|
1758
|
+
var _a2;
|
|
1317
1759
|
if (!routeData.request) return value;
|
|
1318
1760
|
const data = req.data;
|
|
1319
1761
|
if (typeof value === "string") {
|
|
1320
|
-
(
|
|
1762
|
+
(_a2 = value.match(/\$[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a2.forEach((param) => {
|
|
1321
1763
|
const requestParam = routeData.request.find((item) => {
|
|
1322
1764
|
return item.name === param.replace("$", "");
|
|
1323
1765
|
});
|
|
@@ -1330,9 +1772,9 @@ var SqlEngine = class {
|
|
|
1330
1772
|
return value;
|
|
1331
1773
|
}
|
|
1332
1774
|
replaceGlobalParamKeywords(value, routeData, req, sqlParams) {
|
|
1333
|
-
var
|
|
1775
|
+
var _a2;
|
|
1334
1776
|
if (typeof value === "string") {
|
|
1335
|
-
(
|
|
1777
|
+
(_a2 = value.match(/#[a-zA-Z][a-zA-Z0-9_]+/g)) == null ? void 0 : _a2.forEach((param) => {
|
|
1336
1778
|
param = param.replace("#", "");
|
|
1337
1779
|
const globalParamValue = req.requesterDetails[param];
|
|
1338
1780
|
if (!globalParamValue)
|
|
@@ -1351,32 +1793,86 @@ var SqlEngine = class {
|
|
|
1351
1793
|
// src/restura/sql/filterPsqlParser.ts
|
|
1352
1794
|
import peg from "pegjs";
|
|
1353
1795
|
var filterSqlGrammar = `
|
|
1796
|
+
{
|
|
1797
|
+
// ported from pg-format but intentionally will add double quotes to every column
|
|
1798
|
+
function quoteSqlIdentity(value) {
|
|
1799
|
+
if (value === undefined || value === null) {
|
|
1800
|
+
throw new Error('SQL identifier cannot be null or undefined');
|
|
1801
|
+
} else if (value === false) {
|
|
1802
|
+
return '"f"';
|
|
1803
|
+
} else if (value === true) {
|
|
1804
|
+
return '"t"';
|
|
1805
|
+
} else if (value instanceof Date) {
|
|
1806
|
+
// return '"' + formatDate(value.toISOString()) + '"';
|
|
1807
|
+
} else if (value instanceof Buffer) {
|
|
1808
|
+
throw new Error('SQL identifier cannot be a buffer');
|
|
1809
|
+
} else if (Array.isArray(value) === true) {
|
|
1810
|
+
var temp = [];
|
|
1811
|
+
for (var i = 0; i < value.length; i++) {
|
|
1812
|
+
if (Array.isArray(value[i]) === true) {
|
|
1813
|
+
throw new Error('Nested array to grouped list conversion is not supported for SQL identifier');
|
|
1814
|
+
} else {
|
|
1815
|
+
// temp.push(quoteIdent(value[i]));
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
return temp.toString();
|
|
1819
|
+
} else if (value === Object(value)) {
|
|
1820
|
+
throw new Error('SQL identifier cannot be an object');
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
var ident = value.toString().slice(0); // create copy
|
|
1824
|
+
|
|
1825
|
+
// do not quote a valid, unquoted identifier
|
|
1826
|
+
// if (/^[a-z_][a-z0-9_$]*$/.test(ident) === true && isReserved(ident) === false) {
|
|
1827
|
+
// return ident;
|
|
1828
|
+
// }
|
|
1829
|
+
|
|
1830
|
+
var quoted = '"';
|
|
1831
|
+
|
|
1832
|
+
for (var i = 0; i < ident.length; i++) {
|
|
1833
|
+
var c = ident[i];
|
|
1834
|
+
if (c === '"') {
|
|
1835
|
+
quoted += c + c;
|
|
1836
|
+
} else {
|
|
1837
|
+
quoted += c;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
quoted += '"';
|
|
1842
|
+
|
|
1843
|
+
return quoted;
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1354
1847
|
start = expressionList
|
|
1355
1848
|
|
|
1849
|
+
_ = [ \\t\\r\\n]* // Matches spaces, tabs, and line breaks
|
|
1850
|
+
|
|
1356
1851
|
expressionList =
|
|
1357
|
-
|
|
1852
|
+
leftExpression:expression _ operator:operator _ rightExpression:expressionList
|
|
1358
1853
|
{ return \`\${leftExpression} \${operator} \${rightExpression}\`;}
|
|
1359
1854
|
/ expression
|
|
1360
1855
|
|
|
1361
1856
|
expression =
|
|
1362
|
-
negate:negate?"(" "column:"
|
|
1363
|
-
{return \`\${negate? "
|
|
1857
|
+
negate:negate? _ "(" _ "column" _ ":" column:column _ ","? _ value:value? ","? _ type:type? _ ")"_
|
|
1858
|
+
{return \`\${negate? " NOT " : ""}(\${type? type(column, value) : \`\${column} = \${format.literal(value)}\`})\`;}
|
|
1364
1859
|
/
|
|
1365
|
-
negate:negate?"("expression:expressionList")" { return \`\${negate? "
|
|
1860
|
+
negate:negate?"("expression:expressionList")" { return \`\${negate? " NOT " : ""}(\${expression})\`; }
|
|
1366
1861
|
|
|
1367
1862
|
negate = "!"
|
|
1368
1863
|
|
|
1369
1864
|
operator = "and"i / "or"i
|
|
1370
1865
|
|
|
1371
1866
|
|
|
1372
|
-
column = left:text "." right:text { return \`\${
|
|
1867
|
+
column = left:text "." right:text { return \`\${quoteSqlIdentity(left)}.\${quoteSqlIdentity(right)}\`; }
|
|
1373
1868
|
/
|
|
1374
|
-
text:text { return
|
|
1869
|
+
text:text { return quoteSqlIdentity(text); }
|
|
1375
1870
|
|
|
1376
1871
|
|
|
1377
|
-
text = text:[a-z0-9
|
|
1872
|
+
text = text:[a-z0-9 \\t\\r\\n\\-_:@']i+ { return text.join(""); }
|
|
1873
|
+
|
|
1378
1874
|
|
|
1379
|
-
type = "type:" type:typeString { return type; }
|
|
1875
|
+
type = "type" _ ":" _ type:typeString { return type; }
|
|
1380
1876
|
typeString = text:"startsWith" { return function(column, value) { return \`\${column} ILIKE '\${format.literal(value).slice(1,-1)}%'\`; } } /
|
|
1381
1877
|
text:"endsWith" { return function(column, value) { return \`\${column} ILIKE '%\${format.literal(value).slice(1,-1)}'\`; } } /
|
|
1382
1878
|
text:"contains" { return function(column, value) { return \`\${column} ILIKE '%\${format.literal(value).slice(1,-1)}%'\`; } } /
|
|
@@ -1386,8 +1882,9 @@ typeString = text:"startsWith" { return function(column, value) { return \`\${co
|
|
|
1386
1882
|
text:"lessThanEqual" { return function(column, value) { return \`\${column} <= '\${format.literal(value).slice(1,-1)}'\`; } } /
|
|
1387
1883
|
text:"lessThan" { return function(column, value) { return \`\${column} < '\${format.literal(value).slice(1,-1)}'\`; } } /
|
|
1388
1884
|
text:"isNull" { return function(column, value) { return \`isNull(\${column})\`; } }
|
|
1389
|
-
|
|
1390
|
-
value = "value:" value:text { return value; }
|
|
1885
|
+
|
|
1886
|
+
value = "value" _ ":" value:text { return value; }
|
|
1887
|
+
|
|
1391
1888
|
|
|
1392
1889
|
`;
|
|
1393
1890
|
var filterPsqlParser = peg.generate(filterSqlGrammar, {
|
|
@@ -1397,18 +1894,224 @@ var filterPsqlParser = peg.generate(filterSqlGrammar, {
|
|
|
1397
1894
|
var filterPsqlParser_default = filterPsqlParser;
|
|
1398
1895
|
|
|
1399
1896
|
// src/restura/sql/PsqlEngine.ts
|
|
1897
|
+
var { Client, types } = pg2;
|
|
1898
|
+
var systemUser = {
|
|
1899
|
+
role: "",
|
|
1900
|
+
host: "",
|
|
1901
|
+
ipAddress: "",
|
|
1902
|
+
isSystemUser: true
|
|
1903
|
+
};
|
|
1400
1904
|
var PsqlEngine = class extends SqlEngine {
|
|
1401
|
-
constructor(psqlConnectionPool) {
|
|
1905
|
+
constructor(psqlConnectionPool, shouldListenForDbTriggers = false) {
|
|
1402
1906
|
super();
|
|
1403
1907
|
this.psqlConnectionPool = psqlConnectionPool;
|
|
1908
|
+
this.setupPgReturnTypes();
|
|
1909
|
+
if (shouldListenForDbTriggers) {
|
|
1910
|
+
this.setupTriggerListeners = this.listenForDbTriggers();
|
|
1911
|
+
}
|
|
1404
1912
|
}
|
|
1405
|
-
async
|
|
1406
|
-
|
|
1407
|
-
|
|
1913
|
+
async close() {
|
|
1914
|
+
if (this.triggerClient) {
|
|
1915
|
+
await this.triggerClient.end();
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
setupPgReturnTypes() {
|
|
1919
|
+
const TIMESTAMPTZ_OID = 1184;
|
|
1920
|
+
types.setTypeParser(TIMESTAMPTZ_OID, (val) => {
|
|
1921
|
+
return val === null ? null : new Date(val).toISOString();
|
|
1922
|
+
});
|
|
1923
|
+
const BIGINT_OID = 20;
|
|
1924
|
+
types.setTypeParser(BIGINT_OID, (val) => {
|
|
1925
|
+
return val === null ? null : Number(val);
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
async listenForDbTriggers() {
|
|
1929
|
+
this.triggerClient = new Client({
|
|
1930
|
+
user: this.psqlConnectionPool.poolConfig.user,
|
|
1931
|
+
host: this.psqlConnectionPool.poolConfig.host,
|
|
1932
|
+
database: this.psqlConnectionPool.poolConfig.database,
|
|
1933
|
+
password: this.psqlConnectionPool.poolConfig.password,
|
|
1934
|
+
port: this.psqlConnectionPool.poolConfig.port,
|
|
1935
|
+
connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
|
|
1936
|
+
});
|
|
1937
|
+
await this.triggerClient.connect();
|
|
1938
|
+
const promises = [];
|
|
1939
|
+
promises.push(this.triggerClient.query("LISTEN insert"));
|
|
1940
|
+
promises.push(this.triggerClient.query("LISTEN update"));
|
|
1941
|
+
promises.push(this.triggerClient.query("LISTEN delete"));
|
|
1942
|
+
await Promise.all(promises);
|
|
1943
|
+
this.triggerClient.on("notification", async (msg) => {
|
|
1944
|
+
if (msg.channel === "insert" || msg.channel === "update" || msg.channel === "delete") {
|
|
1945
|
+
const payload = ObjectUtils4.safeParse(msg.payload);
|
|
1946
|
+
await this.handleTrigger(payload, msg.channel.toUpperCase());
|
|
1947
|
+
}
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
async handleTrigger(payload, mutationType) {
|
|
1951
|
+
if (payload.queryMetadata && payload.queryMetadata.connectionInstanceId === this.psqlConnectionPool.instanceId) {
|
|
1952
|
+
await eventManager_default.fireActionFromDbTrigger({ queryMetadata: payload.queryMetadata, mutationType }, payload);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
async createDatabaseFromSchema(schema, connection) {
|
|
1956
|
+
const sqlFullStatement = this.generateDatabaseSchemaFromSchema(schema);
|
|
1957
|
+
await connection.runQuery(sqlFullStatement, [], systemUser);
|
|
1958
|
+
return sqlFullStatement;
|
|
1408
1959
|
}
|
|
1409
1960
|
generateDatabaseSchemaFromSchema(schema) {
|
|
1410
|
-
|
|
1411
|
-
|
|
1961
|
+
const sqlStatements = [];
|
|
1962
|
+
const indexes = [];
|
|
1963
|
+
const triggers = [];
|
|
1964
|
+
for (const table of schema.database) {
|
|
1965
|
+
if (table.notify) {
|
|
1966
|
+
triggers.push(this.createInsertTriggers(table.name, table.notify));
|
|
1967
|
+
triggers.push(this.createUpdateTrigger(table.name, table.notify));
|
|
1968
|
+
triggers.push(this.createDeleteTrigger(table.name, table.notify));
|
|
1969
|
+
}
|
|
1970
|
+
let sql = `CREATE TABLE "${table.name}"
|
|
1971
|
+
( `;
|
|
1972
|
+
const tableColumns = [];
|
|
1973
|
+
for (const column of table.columns) {
|
|
1974
|
+
let columnSql = "";
|
|
1975
|
+
columnSql += ` "${column.name}" ${this.schemaToPsqlType(column)}`;
|
|
1976
|
+
let value = column.value;
|
|
1977
|
+
if (column.type === "JSON") value = "";
|
|
1978
|
+
if (column.type === "JSONB") value = "";
|
|
1979
|
+
if (column.type === "DECIMAL" && value) {
|
|
1980
|
+
value = value.replace("-", ",").replace(/['"]/g, "");
|
|
1981
|
+
}
|
|
1982
|
+
if (value && column.type !== "ENUM") {
|
|
1983
|
+
columnSql += `(${value})`;
|
|
1984
|
+
} else if (column.length) columnSql += `(${column.length})`;
|
|
1985
|
+
if (column.isPrimary) {
|
|
1986
|
+
columnSql += " PRIMARY KEY ";
|
|
1987
|
+
}
|
|
1988
|
+
if (column.isUnique) {
|
|
1989
|
+
columnSql += ` CONSTRAINT "${table.name}_${column.name}_unique_index" UNIQUE `;
|
|
1990
|
+
}
|
|
1991
|
+
if (column.isNullable) columnSql += " NULL";
|
|
1992
|
+
else columnSql += " NOT NULL";
|
|
1993
|
+
if (column.default) columnSql += ` DEFAULT ${column.default}`;
|
|
1994
|
+
if (value && column.type === "ENUM") {
|
|
1995
|
+
columnSql += ` CHECK ("${column.name}" IN (${value}))`;
|
|
1996
|
+
}
|
|
1997
|
+
tableColumns.push(columnSql);
|
|
1998
|
+
}
|
|
1999
|
+
sql += tableColumns.join(", \n");
|
|
2000
|
+
for (const index of table.indexes) {
|
|
2001
|
+
if (!index.isPrimaryKey) {
|
|
2002
|
+
let unique = " ";
|
|
2003
|
+
if (index.isUnique) unique = "UNIQUE ";
|
|
2004
|
+
indexes.push(
|
|
2005
|
+
` CREATE ${unique}INDEX "${index.name}" ON "${table.name}" (${index.columns.map((item) => {
|
|
2006
|
+
return `"${item}" ${index.order}`;
|
|
2007
|
+
}).join(", ")});`
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
sql += "\n);";
|
|
2012
|
+
sqlStatements.push(sql);
|
|
2013
|
+
}
|
|
2014
|
+
for (const table of schema.database) {
|
|
2015
|
+
if (!table.foreignKeys.length) continue;
|
|
2016
|
+
const sql = `ALTER TABLE "${table.name}" `;
|
|
2017
|
+
const constraints = [];
|
|
2018
|
+
for (const foreignKey of table.foreignKeys) {
|
|
2019
|
+
let constraint = ` ADD CONSTRAINT "${foreignKey.name}"
|
|
2020
|
+
FOREIGN KEY ("${foreignKey.column}") REFERENCES "${foreignKey.refTable}" ("${foreignKey.refColumn}")`;
|
|
2021
|
+
constraint += ` ON DELETE ${foreignKey.onDelete}`;
|
|
2022
|
+
constraint += ` ON UPDATE ${foreignKey.onUpdate}`;
|
|
2023
|
+
constraints.push(constraint);
|
|
2024
|
+
}
|
|
2025
|
+
sqlStatements.push(sql + constraints.join(",\n") + ";");
|
|
2026
|
+
}
|
|
2027
|
+
for (const table of schema.database) {
|
|
2028
|
+
if (!table.checkConstraints.length) continue;
|
|
2029
|
+
const sql = `ALTER TABLE "${table.name}" `;
|
|
2030
|
+
const constraints = [];
|
|
2031
|
+
for (const check of table.checkConstraints) {
|
|
2032
|
+
const constraint = `ADD CONSTRAINT "${check.name}" CHECK (${check.check})`;
|
|
2033
|
+
constraints.push(constraint);
|
|
2034
|
+
}
|
|
2035
|
+
sqlStatements.push(sql + constraints.join(",\n") + ";");
|
|
2036
|
+
}
|
|
2037
|
+
sqlStatements.push(indexes.join("\n"));
|
|
2038
|
+
sqlStatements.push(triggers.join("\n"));
|
|
2039
|
+
return sqlStatements.join("\n\n");
|
|
2040
|
+
}
|
|
2041
|
+
async getScratchPool() {
|
|
2042
|
+
var _a2, _b;
|
|
2043
|
+
const scratchDbExists = await this.psqlConnectionPool.runQuery(
|
|
2044
|
+
`SELECT *
|
|
2045
|
+
FROM pg_database
|
|
2046
|
+
WHERE datname = '${this.psqlConnectionPool.poolConfig.database}_scratch';`,
|
|
2047
|
+
[],
|
|
2048
|
+
systemUser
|
|
2049
|
+
);
|
|
2050
|
+
if (scratchDbExists.length === 0) {
|
|
2051
|
+
await this.psqlConnectionPool.runQuery(
|
|
2052
|
+
`CREATE DATABASE ${this.psqlConnectionPool.poolConfig.database}_scratch;`,
|
|
2053
|
+
[],
|
|
2054
|
+
systemUser
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
const scratchPool = new PsqlPool({
|
|
2058
|
+
host: this.psqlConnectionPool.poolConfig.host,
|
|
2059
|
+
port: this.psqlConnectionPool.poolConfig.port,
|
|
2060
|
+
user: this.psqlConnectionPool.poolConfig.user,
|
|
2061
|
+
database: this.psqlConnectionPool.poolConfig.database + "_scratch",
|
|
2062
|
+
password: this.psqlConnectionPool.poolConfig.password,
|
|
2063
|
+
max: this.psqlConnectionPool.poolConfig.max,
|
|
2064
|
+
idleTimeoutMillis: this.psqlConnectionPool.poolConfig.idleTimeoutMillis,
|
|
2065
|
+
connectionTimeoutMillis: this.psqlConnectionPool.poolConfig.connectionTimeoutMillis
|
|
2066
|
+
});
|
|
2067
|
+
await scratchPool.runQuery(`DROP SCHEMA public CASCADE;`, [], systemUser);
|
|
2068
|
+
await scratchPool.runQuery(
|
|
2069
|
+
`CREATE SCHEMA public AUTHORIZATION ${this.psqlConnectionPool.poolConfig.user};`,
|
|
2070
|
+
[],
|
|
2071
|
+
systemUser
|
|
2072
|
+
);
|
|
2073
|
+
const schemaComment = await this.psqlConnectionPool.runQuery(
|
|
2074
|
+
`SELECT pg_description.description
|
|
2075
|
+
FROM pg_description
|
|
2076
|
+
JOIN pg_namespace ON pg_namespace.oid = pg_description.objoid
|
|
2077
|
+
WHERE pg_namespace.nspname = 'public';`,
|
|
2078
|
+
[],
|
|
2079
|
+
systemUser
|
|
2080
|
+
);
|
|
2081
|
+
if ((_a2 = schemaComment[0]) == null ? void 0 : _a2.description) {
|
|
2082
|
+
await scratchPool.runQuery(
|
|
2083
|
+
`COMMENT ON SCHEMA public IS '${(_b = schemaComment[0]) == null ? void 0 : _b.description}';`,
|
|
2084
|
+
[],
|
|
2085
|
+
systemUser
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
return scratchPool;
|
|
2089
|
+
}
|
|
2090
|
+
async diffDatabaseToSchema(schema) {
|
|
2091
|
+
const scratchPool = await this.getScratchPool();
|
|
2092
|
+
await this.createDatabaseFromSchema(schema, scratchPool);
|
|
2093
|
+
const originalClient = new Client({
|
|
2094
|
+
database: this.psqlConnectionPool.poolConfig.database,
|
|
2095
|
+
user: this.psqlConnectionPool.poolConfig.user,
|
|
2096
|
+
password: this.psqlConnectionPool.poolConfig.password,
|
|
2097
|
+
host: this.psqlConnectionPool.poolConfig.host,
|
|
2098
|
+
port: this.psqlConnectionPool.poolConfig.port
|
|
2099
|
+
});
|
|
2100
|
+
const scratchClient = new Client({
|
|
2101
|
+
database: this.psqlConnectionPool.poolConfig.database + "_scratch",
|
|
2102
|
+
user: this.psqlConnectionPool.poolConfig.user,
|
|
2103
|
+
password: this.psqlConnectionPool.poolConfig.password,
|
|
2104
|
+
host: this.psqlConnectionPool.poolConfig.host,
|
|
2105
|
+
port: this.psqlConnectionPool.poolConfig.port
|
|
2106
|
+
});
|
|
2107
|
+
const promises = [originalClient.connect(), scratchClient.connect()];
|
|
2108
|
+
await Promise.all(promises);
|
|
2109
|
+
const infoPromises = [pgInfo({ client: originalClient }), pgInfo({ client: scratchClient })];
|
|
2110
|
+
const [info1, info2] = await Promise.all(infoPromises);
|
|
2111
|
+
const diff = getDiff(info1, info2);
|
|
2112
|
+
const endPromises = [originalClient.end(), scratchClient.end()];
|
|
2113
|
+
await Promise.all(endPromises);
|
|
2114
|
+
return diff.join("\n");
|
|
1412
2115
|
}
|
|
1413
2116
|
createNestedSelect(req, schema, item, routeData, userRole, sqlParams) {
|
|
1414
2117
|
if (!item.subquery) return "";
|
|
@@ -1422,8 +2125,7 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
1422
2125
|
)) {
|
|
1423
2126
|
return "'[]'";
|
|
1424
2127
|
}
|
|
1425
|
-
return `COALESCE((
|
|
1426
|
-
SELECT JSON_AGG(JSON_BUILD_OBJECT(
|
|
2128
|
+
return `COALESCE((SELECT JSON_AGG(JSON_BUILD_OBJECT(
|
|
1427
2129
|
${item.subquery.properties.map((nestedItem) => {
|
|
1428
2130
|
if (!this.doesRoleHavePermissionToColumn(req.requesterDetails.role, schema, nestedItem, [
|
|
1429
2131
|
...routeData.joins,
|
|
@@ -1432,7 +2134,7 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
1432
2134
|
return;
|
|
1433
2135
|
}
|
|
1434
2136
|
if (nestedItem.subquery) {
|
|
1435
|
-
return `
|
|
2137
|
+
return `'${nestedItem.name}', ${this.createNestedSelect(
|
|
1436
2138
|
// recursion
|
|
1437
2139
|
req,
|
|
1438
2140
|
schema,
|
|
@@ -1443,7 +2145,7 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
1443
2145
|
)}`;
|
|
1444
2146
|
}
|
|
1445
2147
|
return `'${nestedItem.name}', ${escapeColumnName(nestedItem.selector)}`;
|
|
1446
|
-
}).filter(Boolean).join(",")}
|
|
2148
|
+
}).filter(Boolean).join(", ")}
|
|
1447
2149
|
))
|
|
1448
2150
|
FROM
|
|
1449
2151
|
"${item.subquery.table}"
|
|
@@ -1458,16 +2160,19 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
1458
2160
|
parameterObj[assignment.name] = this.replaceParamKeywords(assignment.value, routeData, req, sqlParams);
|
|
1459
2161
|
});
|
|
1460
2162
|
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
|
-
|
|
2163
|
+
const createdItem = await this.psqlConnectionPool.queryOne(
|
|
2164
|
+
query,
|
|
2165
|
+
sqlParams,
|
|
2166
|
+
req.requesterDetails
|
|
2167
|
+
);
|
|
2168
|
+
const insertId = createdItem.id;
|
|
2169
|
+
const whereId = {
|
|
2170
|
+
tableName: routeData.table,
|
|
2171
|
+
value: insertId,
|
|
2172
|
+
columnName: "id",
|
|
2173
|
+
operator: "="
|
|
2174
|
+
};
|
|
2175
|
+
const whereData = [whereId];
|
|
1471
2176
|
req.data = { id: insertId };
|
|
1472
2177
|
return this.executeGetRequest(req, __spreadProps(__spreadValues({}, routeData), { where: whereData }), schema);
|
|
1473
2178
|
}
|
|
@@ -1486,7 +2191,9 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
1486
2191
|
let selectStatement = "SELECT \n";
|
|
1487
2192
|
selectStatement += ` ${selectColumns.map((item) => {
|
|
1488
2193
|
if (item.subquery) {
|
|
1489
|
-
return `${this.createNestedSelect(req, schema, item, routeData, userRole, sqlParams)} AS ${
|
|
2194
|
+
return `${this.createNestedSelect(req, schema, item, routeData, userRole, sqlParams)} AS ${escapeColumnName(
|
|
2195
|
+
item.name
|
|
2196
|
+
)}`;
|
|
1490
2197
|
}
|
|
1491
2198
|
return `${escapeColumnName(item.selector)} AS ${escapeColumnName(item.name)}`;
|
|
1492
2199
|
}).join(",\n ")}
|
|
@@ -1519,29 +2226,31 @@ var PsqlEngine = class extends SqlEngine {
|
|
|
1519
2226
|
);
|
|
1520
2227
|
} else if (routeData.type === "PAGED") {
|
|
1521
2228
|
const data = req.data;
|
|
1522
|
-
const
|
|
1523
|
-
`${selectStatement}${sqlStatement}${groupByOrderByStatement} LIMIT
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
2229
|
+
const pagePromise = this.psqlConnectionPool.runQuery(
|
|
2230
|
+
`${selectStatement}${sqlStatement}${groupByOrderByStatement}` + SQL`LIMIT ${data.perPage || DEFAULT_PAGED_PER_PAGE_NUMBER} OFFSET ${(data.page - 1) * data.perPage || DEFAULT_PAGED_PAGE_NUMBER};`,
|
|
2231
|
+
sqlParams,
|
|
2232
|
+
req.requesterDetails
|
|
2233
|
+
);
|
|
2234
|
+
const totalQuery = `SELECT COUNT(${routeData.groupBy ? `DISTINCT ${routeData.groupBy.tableName}.${routeData.groupBy.columnName}` : "*"}) AS total
|
|
2235
|
+
${sqlStatement};`;
|
|
2236
|
+
const totalPromise = this.psqlConnectionPool.runQuery(
|
|
2237
|
+
totalQuery,
|
|
2238
|
+
sqlParams,
|
|
1531
2239
|
req.requesterDetails
|
|
1532
2240
|
);
|
|
2241
|
+
const [pageResults, totalResponse] = await Promise.all([pagePromise, totalPromise]);
|
|
1533
2242
|
let total = 0;
|
|
1534
|
-
if (ObjectUtils4.isArrayWithData(
|
|
1535
|
-
total =
|
|
2243
|
+
if (ObjectUtils4.isArrayWithData(totalResponse)) {
|
|
2244
|
+
total = totalResponse[0].total;
|
|
1536
2245
|
}
|
|
1537
|
-
return { data: pageResults
|
|
2246
|
+
return { data: pageResults, total };
|
|
1538
2247
|
} else {
|
|
1539
2248
|
throw new RsError("UNKNOWN_ERROR", "Unknown route type.");
|
|
1540
2249
|
}
|
|
1541
2250
|
}
|
|
1542
2251
|
async executeUpdateRequest(req, routeData, schema) {
|
|
1543
2252
|
const sqlParams = [];
|
|
1544
|
-
const
|
|
2253
|
+
const _a2 = req.body, { id } = _a2, bodyNoId = __objRest(_a2, ["id"]);
|
|
1545
2254
|
const table = schema.database.find((item) => {
|
|
1546
2255
|
return item.name === routeData.table;
|
|
1547
2256
|
});
|
|
@@ -1573,10 +2282,12 @@ ${sqlStatement};`,
|
|
|
1573
2282
|
req.requesterDetails.role,
|
|
1574
2283
|
sqlParams
|
|
1575
2284
|
);
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
2285
|
+
const whereClause = this.generateWhereClause(req, routeData.where, routeData, sqlParams);
|
|
2286
|
+
if (whereClause.replace(/\s/g, "") === "") {
|
|
2287
|
+
throw new RsError("DELETE_FORBIDDEN", "Deletes need a where clause");
|
|
2288
|
+
}
|
|
2289
|
+
const deleteStatement = `
|
|
2290
|
+
DELETE FROM "${routeData.table}" ${joinStatement} ${whereClause}`;
|
|
1580
2291
|
await this.psqlConnectionPool.runQuery(deleteStatement, sqlParams, req.requesterDetails);
|
|
1581
2292
|
return true;
|
|
1582
2293
|
}
|
|
@@ -1639,38 +2350,37 @@ ${sqlStatement};`,
|
|
|
1639
2350
|
);
|
|
1640
2351
|
let operator = item.operator;
|
|
1641
2352
|
if (operator === "LIKE") {
|
|
1642
|
-
|
|
2353
|
+
item.value = `'%${item.value}%'`;
|
|
1643
2354
|
} else if (operator === "STARTS WITH") {
|
|
1644
2355
|
operator = "LIKE";
|
|
1645
|
-
|
|
2356
|
+
item.value = `'${item.value}%'`;
|
|
1646
2357
|
} else if (operator === "ENDS WITH") {
|
|
1647
2358
|
operator = "LIKE";
|
|
1648
|
-
|
|
2359
|
+
item.value = `'%${item.value}'`;
|
|
1649
2360
|
}
|
|
1650
2361
|
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}
|
|
2362
|
+
whereClause += ` ${item.conjunction || ""} "${item.tableName}"."${item.columnName}" ${operator.replace("LIKE", "ILIKE")} ${["IN", "NOT IN"].includes(operator) ? `(${replacedValue})` : replacedValue}
|
|
1653
2363
|
`;
|
|
1654
2364
|
});
|
|
1655
2365
|
const data = req.data;
|
|
1656
2366
|
if (routeData.type === "PAGED" && !!(data == null ? void 0 : data.filter)) {
|
|
1657
2367
|
let statement = data.filter.replace(/\$[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
|
|
1658
|
-
var
|
|
2368
|
+
var _a2;
|
|
1659
2369
|
const requestParam = routeData.request.find((item) => {
|
|
1660
2370
|
return item.name === value.replace("$", "");
|
|
1661
2371
|
});
|
|
1662
2372
|
if (!requestParam)
|
|
1663
2373
|
throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
|
|
1664
|
-
return ((
|
|
2374
|
+
return ((_a2 = data[requestParam.name]) == null ? void 0 : _a2.toString()) || "";
|
|
1665
2375
|
});
|
|
1666
2376
|
statement = statement.replace(/#[a-zA-Z][a-zA-Z0-9_]+/g, (value) => {
|
|
1667
|
-
var
|
|
2377
|
+
var _a2;
|
|
1668
2378
|
const requestParam = routeData.request.find((item) => {
|
|
1669
2379
|
return item.name === value.replace("#", "");
|
|
1670
2380
|
});
|
|
1671
2381
|
if (!requestParam)
|
|
1672
2382
|
throw new RsError("SCHEMA_ERROR", `Invalid route keyword in route ${routeData.name}`);
|
|
1673
|
-
return ((
|
|
2383
|
+
return ((_a2 = data[requestParam.name]) == null ? void 0 : _a2.toString()) || "";
|
|
1674
2384
|
});
|
|
1675
2385
|
statement = filterPsqlParser_default.parse(statement);
|
|
1676
2386
|
if (whereClause.startsWith("WHERE")) {
|
|
@@ -1683,82 +2393,259 @@ ${sqlStatement};`,
|
|
|
1683
2393
|
}
|
|
1684
2394
|
return whereClause;
|
|
1685
2395
|
}
|
|
1686
|
-
|
|
2396
|
+
createUpdateTrigger(tableName, notify) {
|
|
2397
|
+
if (!notify) return "";
|
|
2398
|
+
if (notify === "ALL") {
|
|
2399
|
+
return `
|
|
2400
|
+
CREATE OR REPLACE FUNCTION notify_${tableName}_update()
|
|
2401
|
+
RETURNS TRIGGER AS $$
|
|
2402
|
+
DECLARE
|
|
2403
|
+
query_metadata JSON;
|
|
2404
|
+
BEGIN
|
|
2405
|
+
SELECT INTO query_metadata
|
|
2406
|
+
(regexp_match(
|
|
2407
|
+
current_query(),
|
|
2408
|
+
'^--QUERY_METADATA\\(({.*})', 'n'
|
|
2409
|
+
))[1]::json;
|
|
1687
2410
|
|
|
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}`);
|
|
2411
|
+
PERFORM pg_notify(
|
|
2412
|
+
'update',
|
|
2413
|
+
json_build_object(
|
|
2414
|
+
'table', '${tableName}',
|
|
2415
|
+
'queryMetadata', query_metadata,
|
|
2416
|
+
'changedId', NEW.id,
|
|
2417
|
+
'record', NEW,
|
|
2418
|
+
'previousRecord', OLD
|
|
2419
|
+
)::text
|
|
2420
|
+
);
|
|
2421
|
+
RETURN NEW;
|
|
2422
|
+
END;
|
|
2423
|
+
$$ LANGUAGE plpgsql;
|
|
2424
|
+
|
|
2425
|
+
CREATE OR REPLACE TRIGGER ${tableName}_update
|
|
2426
|
+
AFTER UPDATE ON "${tableName}"
|
|
2427
|
+
FOR EACH ROW
|
|
2428
|
+
EXECUTE FUNCTION notify_${tableName}_update();
|
|
2429
|
+
`;
|
|
1719
2430
|
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
2431
|
+
const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
|
|
2432
|
+
const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
|
|
2433
|
+
return `
|
|
2434
|
+
CREATE OR REPLACE FUNCTION notify_${tableName}_update()
|
|
2435
|
+
RETURNS TRIGGER AS $$
|
|
2436
|
+
DECLARE
|
|
2437
|
+
query_metadata JSON;
|
|
2438
|
+
BEGIN
|
|
2439
|
+
SELECT INTO query_metadata
|
|
2440
|
+
(regexp_match(
|
|
2441
|
+
current_query(),
|
|
2442
|
+
'^--QUERY_METADATA\\(({.*})', 'n'
|
|
2443
|
+
))[1]::json;
|
|
2444
|
+
|
|
2445
|
+
PERFORM pg_notify(
|
|
2446
|
+
'update',
|
|
2447
|
+
json_build_object(
|
|
2448
|
+
'table', '${tableName}',
|
|
2449
|
+
'queryMetadata', query_metadata,
|
|
2450
|
+
'changedId', NEW.id,
|
|
2451
|
+
'record', json_build_object(
|
|
2452
|
+
${notifyColumnNewBuildString}
|
|
2453
|
+
),
|
|
2454
|
+
'previousRecord', json_build_object(
|
|
2455
|
+
${notifyColumnOldBuildString}
|
|
2456
|
+
)
|
|
2457
|
+
)::text
|
|
2458
|
+
);
|
|
2459
|
+
RETURN NEW;
|
|
2460
|
+
END;
|
|
2461
|
+
$$ LANGUAGE plpgsql;
|
|
2462
|
+
|
|
2463
|
+
CREATE OR REPLACE TRIGGER ${tableName}_update
|
|
2464
|
+
AFTER UPDATE ON "${tableName}"
|
|
2465
|
+
FOR EACH ROW
|
|
2466
|
+
EXECUTE FUNCTION notify_${tableName}_update();
|
|
2467
|
+
`;
|
|
2468
|
+
}
|
|
2469
|
+
createDeleteTrigger(tableName, notify) {
|
|
2470
|
+
if (!notify) return "";
|
|
2471
|
+
if (notify === "ALL") {
|
|
2472
|
+
return `
|
|
2473
|
+
CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
|
|
2474
|
+
RETURNS TRIGGER AS $$
|
|
2475
|
+
DECLARE
|
|
2476
|
+
query_metadata JSON;
|
|
2477
|
+
BEGIN
|
|
2478
|
+
SELECT INTO query_metadata
|
|
2479
|
+
(regexp_match(
|
|
2480
|
+
current_query(),
|
|
2481
|
+
'^--QUERY_METADATA\\(({.*})', 'n'
|
|
2482
|
+
))[1]::json;
|
|
2483
|
+
|
|
2484
|
+
PERFORM pg_notify(
|
|
2485
|
+
'delete',
|
|
2486
|
+
json_build_object(
|
|
2487
|
+
'table', '${tableName}',
|
|
2488
|
+
'queryMetadata', query_metadata,
|
|
2489
|
+
'deletedId', OLD.id,
|
|
2490
|
+
'previousRecord', OLD
|
|
2491
|
+
)::text
|
|
2492
|
+
);
|
|
2493
|
+
RETURN NEW;
|
|
2494
|
+
END;
|
|
2495
|
+
$$ LANGUAGE plpgsql;
|
|
2496
|
+
|
|
2497
|
+
CREATE OR REPLACE TRIGGER "${tableName}_delete"
|
|
2498
|
+
AFTER DELETE ON "${tableName}"
|
|
2499
|
+
FOR EACH ROW
|
|
2500
|
+
EXECUTE FUNCTION notify_${tableName}_delete();
|
|
2501
|
+
`;
|
|
1736
2502
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
2503
|
+
const notifyColumnOldBuildString = notify.map((column) => `'${column}', OLD."${column}"`).join(",\n");
|
|
2504
|
+
return `
|
|
2505
|
+
CREATE OR REPLACE FUNCTION notify_${tableName}_delete()
|
|
2506
|
+
RETURNS TRIGGER AS $$
|
|
2507
|
+
DECLARE
|
|
2508
|
+
query_metadata JSON;
|
|
2509
|
+
BEGIN
|
|
2510
|
+
SELECT INTO query_metadata
|
|
2511
|
+
(regexp_match(
|
|
2512
|
+
current_query(),
|
|
2513
|
+
'^--QUERY_METADATA\\(({.*})', 'n'
|
|
2514
|
+
))[1]::json;
|
|
2515
|
+
|
|
2516
|
+
PERFORM pg_notify(
|
|
2517
|
+
'delete',
|
|
2518
|
+
json_build_object(
|
|
2519
|
+
'table', '${tableName}',
|
|
2520
|
+
'queryMetadata', query_metadata,
|
|
2521
|
+
'deletedId', OLD.id,
|
|
2522
|
+
'previousRecord', json_build_object(
|
|
2523
|
+
${notifyColumnOldBuildString}
|
|
2524
|
+
)
|
|
2525
|
+
)::text
|
|
2526
|
+
);
|
|
2527
|
+
RETURN NEW;
|
|
2528
|
+
END;
|
|
2529
|
+
$$ LANGUAGE plpgsql;
|
|
2530
|
+
|
|
2531
|
+
CREATE OR REPLACE TRIGGER "${tableName}_delete"
|
|
2532
|
+
AFTER DELETE ON "${tableName}"
|
|
2533
|
+
FOR EACH ROW
|
|
2534
|
+
EXECUTE FUNCTION notify_${tableName}_delete();
|
|
2535
|
+
`;
|
|
2536
|
+
}
|
|
2537
|
+
createInsertTriggers(tableName, notify) {
|
|
2538
|
+
if (!notify) return "";
|
|
2539
|
+
if (notify === "ALL") {
|
|
2540
|
+
return `
|
|
2541
|
+
CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
|
|
2542
|
+
RETURNS TRIGGER AS $$
|
|
2543
|
+
DECLARE
|
|
2544
|
+
query_metadata JSON;
|
|
2545
|
+
BEGIN
|
|
2546
|
+
SELECT INTO query_metadata
|
|
2547
|
+
(regexp_match(
|
|
2548
|
+
current_query(),
|
|
2549
|
+
'^--QUERY_METADATA\\(({.*})', 'n'
|
|
2550
|
+
))[1]::json;
|
|
2551
|
+
|
|
2552
|
+
PERFORM pg_notify(
|
|
2553
|
+
'insert',
|
|
2554
|
+
json_build_object(
|
|
2555
|
+
'table', '${tableName}',
|
|
2556
|
+
'queryMetadata', query_metadata,
|
|
2557
|
+
'insertedId', NEW.id,
|
|
2558
|
+
'record', NEW
|
|
2559
|
+
)::text
|
|
2560
|
+
);
|
|
2561
|
+
|
|
2562
|
+
RETURN NEW;
|
|
2563
|
+
END;
|
|
2564
|
+
$$ LANGUAGE plpgsql;
|
|
2565
|
+
|
|
2566
|
+
CREATE OR REPLACE TRIGGER "${tableName}_insert"
|
|
2567
|
+
AFTER INSERT ON "${tableName}"
|
|
2568
|
+
FOR EACH ROW
|
|
2569
|
+
EXECUTE FUNCTION notify_${tableName}_insert();
|
|
2570
|
+
`;
|
|
1750
2571
|
}
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
2572
|
+
const notifyColumnNewBuildString = notify.map((column) => `'${column}', NEW."${column}"`).join(",\n");
|
|
2573
|
+
return `
|
|
2574
|
+
CREATE OR REPLACE FUNCTION notify_${tableName}_insert()
|
|
2575
|
+
RETURNS TRIGGER AS $$
|
|
2576
|
+
DECLARE
|
|
2577
|
+
query_metadata JSON;
|
|
2578
|
+
BEGIN
|
|
2579
|
+
SELECT INTO query_metadata
|
|
2580
|
+
(regexp_match(
|
|
2581
|
+
current_query(),
|
|
2582
|
+
'^--QUERY_METADATA\\(({.*})', 'n'
|
|
2583
|
+
))[1]::json;
|
|
2584
|
+
|
|
2585
|
+
PERFORM pg_notify(
|
|
2586
|
+
'insert',
|
|
2587
|
+
json_build_object(
|
|
2588
|
+
'table', '${tableName}',
|
|
2589
|
+
'queryMetadata', query_metadata,
|
|
2590
|
+
'insertedId', NEW.id,
|
|
2591
|
+
'record', json_build_object(
|
|
2592
|
+
${notifyColumnNewBuildString}
|
|
2593
|
+
)
|
|
2594
|
+
)::text
|
|
2595
|
+
);
|
|
2596
|
+
|
|
2597
|
+
RETURN NEW;
|
|
2598
|
+
END;
|
|
2599
|
+
$$ LANGUAGE plpgsql;
|
|
2600
|
+
|
|
2601
|
+
CREATE OR REPLACE TRIGGER "${tableName}_insert"
|
|
2602
|
+
AFTER INSERT ON "${tableName}"
|
|
2603
|
+
FOR EACH ROW
|
|
2604
|
+
EXECUTE FUNCTION notify_${tableName}_insert();
|
|
2605
|
+
`;
|
|
2606
|
+
}
|
|
2607
|
+
schemaToPsqlType(column) {
|
|
2608
|
+
if (column.hasAutoIncrement) return "BIGSERIAL";
|
|
2609
|
+
if (column.type === "ENUM") return `TEXT`;
|
|
2610
|
+
if (column.type === "DATETIME") return "TIMESTAMPTZ";
|
|
2611
|
+
if (column.type === "MEDIUMINT") return "INT";
|
|
2612
|
+
return column.type;
|
|
2613
|
+
}
|
|
2614
|
+
};
|
|
2615
|
+
|
|
2616
|
+
// src/restura/utils/TempCache.ts
|
|
2617
|
+
import fs3 from "fs";
|
|
2618
|
+
import path3 from "path";
|
|
2619
|
+
import { DateUtils } from "@redskytech/core-utils";
|
|
2620
|
+
import { FileUtils as FileUtils2 } from "@restura/internal";
|
|
2621
|
+
import Bluebird3 from "bluebird";
|
|
2622
|
+
import * as os2 from "os";
|
|
2623
|
+
var TempCache = class {
|
|
2624
|
+
constructor(location) {
|
|
2625
|
+
this.maxDurationDays = 7;
|
|
2626
|
+
this.location = location || os2.tmpdir();
|
|
2627
|
+
FileUtils2.ensureDir(this.location).catch((e) => {
|
|
2628
|
+
throw e;
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2631
|
+
async cleanup() {
|
|
2632
|
+
const fileList = await fs3.promises.readdir(this.location);
|
|
2633
|
+
await Bluebird3.map(
|
|
2634
|
+
fileList,
|
|
2635
|
+
async (file) => {
|
|
2636
|
+
const fullFilePath = path3.join(this.location, file);
|
|
2637
|
+
const fileStats = await fs3.promises.stat(fullFilePath);
|
|
2638
|
+
if (DateUtils.daysBetweenStartAndEndDates(new Date(fileStats.mtimeMs), /* @__PURE__ */ new Date()) > this.maxDurationDays) {
|
|
2639
|
+
logger.info(`Deleting old temp file: ${file}`);
|
|
2640
|
+
await fs3.promises.unlink(fullFilePath);
|
|
2641
|
+
}
|
|
2642
|
+
},
|
|
2643
|
+
{ concurrency: 10 }
|
|
2644
|
+
);
|
|
1757
2645
|
}
|
|
1758
2646
|
};
|
|
1759
2647
|
|
|
1760
2648
|
// src/restura/restura.ts
|
|
1761
|
-
var { types } = pg2;
|
|
1762
2649
|
var ResturaEngine = class {
|
|
1763
2650
|
constructor() {
|
|
1764
2651
|
this.publicEndpoints = {
|
|
@@ -1777,9 +2664,10 @@ var ResturaEngine = class {
|
|
|
1777
2664
|
*/
|
|
1778
2665
|
async init(app, authenticationHandler, psqlConnectionPool) {
|
|
1779
2666
|
this.resturaConfig = config2.validate("restura", resturaConfigSchema);
|
|
2667
|
+
this.multerCommonUpload = getMulterUpload(this.resturaConfig.fileTempCachePath);
|
|
2668
|
+
new TempCache(this.resturaConfig.fileTempCachePath);
|
|
1780
2669
|
this.psqlConnectionPool = psqlConnectionPool;
|
|
1781
|
-
this.psqlEngine = new PsqlEngine(this.psqlConnectionPool);
|
|
1782
|
-
setupPgReturnTypes();
|
|
2670
|
+
this.psqlEngine = new PsqlEngine(this.psqlConnectionPool, true);
|
|
1783
2671
|
await customApiFactory_default.loadApiFiles(this.resturaConfig.customApiFolderPath);
|
|
1784
2672
|
this.authenticationHandler = authenticationHandler;
|
|
1785
2673
|
app.use(compression());
|
|
@@ -1843,10 +2731,7 @@ var ResturaEngine = class {
|
|
|
1843
2731
|
* @returns A promise that resolves when the API has been successfully generated and written to the output file.
|
|
1844
2732
|
*/
|
|
1845
2733
|
async generateApiFromSchema(outputFile, providedSchema) {
|
|
1846
|
-
|
|
1847
|
-
outputFile,
|
|
1848
|
-
await apiGenerator(providedSchema, await this.generateHashForSchema(providedSchema))
|
|
1849
|
-
);
|
|
2734
|
+
fs4.writeFileSync(outputFile, await apiGenerator(providedSchema));
|
|
1850
2735
|
}
|
|
1851
2736
|
/**
|
|
1852
2737
|
* Generates a model from the provided schema and writes it to the specified output file.
|
|
@@ -1856,10 +2741,15 @@ var ResturaEngine = class {
|
|
|
1856
2741
|
* @returns A promise that resolves when the model has been successfully written to the output file.
|
|
1857
2742
|
*/
|
|
1858
2743
|
async generateModelFromSchema(outputFile, providedSchema) {
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
2744
|
+
fs4.writeFileSync(outputFile, await modelGenerator(providedSchema));
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Generates the ambient module declaration for Restura global types and writes it to the specified output file.
|
|
2748
|
+
* These types are used sometimes in the CustomTypes
|
|
2749
|
+
* @param outputFile
|
|
2750
|
+
*/
|
|
2751
|
+
generateResturaGlobalTypes(outputFile) {
|
|
2752
|
+
fs4.writeFileSync(outputFile, resturaGlobalTypesGenerator());
|
|
1863
2753
|
}
|
|
1864
2754
|
/**
|
|
1865
2755
|
* Retrieves the latest file system schema for Restura.
|
|
@@ -1868,11 +2758,11 @@ var ResturaEngine = class {
|
|
|
1868
2758
|
* @throws {Error} If the schema file is missing or the schema is not valid.
|
|
1869
2759
|
*/
|
|
1870
2760
|
async getLatestFileSystemSchema() {
|
|
1871
|
-
if (!
|
|
2761
|
+
if (!fs4.existsSync(this.resturaConfig.schemaFilePath)) {
|
|
1872
2762
|
logger.error(`Missing restura schema file, expected path: ${this.resturaConfig.schemaFilePath}`);
|
|
1873
2763
|
throw new Error("Missing restura schema file");
|
|
1874
2764
|
}
|
|
1875
|
-
const schemaFileData =
|
|
2765
|
+
const schemaFileData = fs4.readFileSync(this.resturaConfig.schemaFilePath, { encoding: "utf8" });
|
|
1876
2766
|
const schema = ObjectUtils5.safeParse(schemaFileData);
|
|
1877
2767
|
const isValid = await isSchemaValid(schema);
|
|
1878
2768
|
if (!isValid) {
|
|
@@ -1881,28 +2771,6 @@ var ResturaEngine = class {
|
|
|
1881
2771
|
}
|
|
1882
2772
|
return schema;
|
|
1883
2773
|
}
|
|
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
2774
|
async reloadEndpoints() {
|
|
1907
2775
|
this.schema = await this.getLatestFileSystemSchema();
|
|
1908
2776
|
this.customTypeValidation = customTypeValidationGenerator(this.schema);
|
|
@@ -1931,30 +2799,10 @@ var ResturaEngine = class {
|
|
|
1931
2799
|
logger.info(`Restura loaded (${routeCount}) endpoint${routeCount > 1 ? "s" : ""}`);
|
|
1932
2800
|
}
|
|
1933
2801
|
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
|
-
);
|
|
2802
|
+
if (!fs4.existsSync(this.resturaConfig.generatedTypesPath)) {
|
|
2803
|
+
fs4.mkdirSync(this.resturaConfig.generatedTypesPath, { recursive: true });
|
|
1957
2804
|
}
|
|
2805
|
+
this.updateTypes();
|
|
1958
2806
|
}
|
|
1959
2807
|
resturaAuthentication(req, res, next) {
|
|
1960
2808
|
if (req.headers["x-auth-token"] !== this.resturaConfig.authToken) res.status(401).send("Unauthorized");
|
|
@@ -1962,7 +2810,7 @@ var ResturaEngine = class {
|
|
|
1962
2810
|
}
|
|
1963
2811
|
async previewCreateSchema(req, res) {
|
|
1964
2812
|
try {
|
|
1965
|
-
const schemaDiff =
|
|
2813
|
+
const schemaDiff = await compareSchema_default.diffSchema(req.data, this.schema, this.psqlEngine);
|
|
1966
2814
|
res.send({ data: schemaDiff });
|
|
1967
2815
|
} catch (err) {
|
|
1968
2816
|
res.status(400).send(err);
|
|
@@ -1970,7 +2818,7 @@ var ResturaEngine = class {
|
|
|
1970
2818
|
}
|
|
1971
2819
|
async updateSchema(req, res) {
|
|
1972
2820
|
try {
|
|
1973
|
-
this.schema = req.data;
|
|
2821
|
+
this.schema = sortObjectKeysAlphabetically(req.data);
|
|
1974
2822
|
await this.storeFileSystemSchema();
|
|
1975
2823
|
await this.reloadEndpoints();
|
|
1976
2824
|
await this.updateTypes();
|
|
@@ -1981,11 +2829,12 @@ var ResturaEngine = class {
|
|
|
1981
2829
|
}
|
|
1982
2830
|
}
|
|
1983
2831
|
async updateTypes() {
|
|
1984
|
-
await this.generateApiFromSchema(
|
|
2832
|
+
await this.generateApiFromSchema(path4.join(this.resturaConfig.generatedTypesPath, "api.d.ts"), this.schema);
|
|
1985
2833
|
await this.generateModelFromSchema(
|
|
1986
|
-
|
|
2834
|
+
path4.join(this.resturaConfig.generatedTypesPath, "models.d.ts"),
|
|
1987
2835
|
this.schema
|
|
1988
2836
|
);
|
|
2837
|
+
this.generateResturaGlobalTypes(path4.join(this.resturaConfig.generatedTypesPath, "restura.d.ts"));
|
|
1989
2838
|
}
|
|
1990
2839
|
async getSchema(req, res) {
|
|
1991
2840
|
res.send({ data: this.schema });
|
|
@@ -1993,19 +2842,38 @@ var ResturaEngine = class {
|
|
|
1993
2842
|
async getSchemaAndTypes(req, res) {
|
|
1994
2843
|
try {
|
|
1995
2844
|
const schema = await this.getLatestFileSystemSchema();
|
|
1996
|
-
const
|
|
1997
|
-
const
|
|
1998
|
-
const modelsText = await modelGenerator(schema, schemaHash);
|
|
2845
|
+
const apiText = await apiGenerator(schema);
|
|
2846
|
+
const modelsText = await modelGenerator(schema);
|
|
1999
2847
|
res.send({ schema, api: apiText, models: modelsText });
|
|
2000
2848
|
} catch (err) {
|
|
2001
2849
|
res.status(400).send({ error: err });
|
|
2002
2850
|
}
|
|
2003
2851
|
}
|
|
2852
|
+
async getMulterFilesIfAny(req, res, routeData) {
|
|
2853
|
+
var _a2;
|
|
2854
|
+
if (!((_a2 = req.header("content-type")) == null ? void 0 : _a2.includes("multipart/form-data"))) return;
|
|
2855
|
+
if (!this.isCustomRoute(routeData)) return;
|
|
2856
|
+
if (!routeData.fileUploadType) {
|
|
2857
|
+
throw new RsError("BAD_REQUEST", "File upload type not defined for route");
|
|
2858
|
+
}
|
|
2859
|
+
const multerFileUploadFunction = routeData.fileUploadType === "MULTIPLE" ? this.multerCommonUpload.array("files") : this.multerCommonUpload.single("file");
|
|
2860
|
+
return new Promise((resolve2, reject) => {
|
|
2861
|
+
multerFileUploadFunction(req, res, (err) => {
|
|
2862
|
+
if (err) {
|
|
2863
|
+
logger.warn("Multer error: " + err);
|
|
2864
|
+
reject(err);
|
|
2865
|
+
}
|
|
2866
|
+
if (req.body["data"]) req.body = JSON.parse(req.body["data"]);
|
|
2867
|
+
resolve2();
|
|
2868
|
+
});
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2004
2871
|
async executeRouteLogic(req, res, next) {
|
|
2005
2872
|
try {
|
|
2006
2873
|
const routeData = this.getRouteData(req.method, req.baseUrl, req.path);
|
|
2007
2874
|
this.validateAuthorization(req, routeData);
|
|
2008
|
-
|
|
2875
|
+
await this.getMulterFilesIfAny(req, res, routeData);
|
|
2876
|
+
requestValidator(req, routeData, this.customTypeValidation);
|
|
2009
2877
|
if (this.isCustomRoute(routeData)) {
|
|
2010
2878
|
await this.runCustomRouteLogic(req, res, routeData);
|
|
2011
2879
|
return;
|
|
@@ -2041,22 +2909,10 @@ var ResturaEngine = class {
|
|
|
2041
2909
|
return acc + StringUtils3.capitalizeFirst(cur);
|
|
2042
2910
|
}, "")}`;
|
|
2043
2911
|
const customFunction = customApi[functionName];
|
|
2044
|
-
if (!customFunction)
|
|
2912
|
+
if (!customFunction)
|
|
2913
|
+
throw new RsError("NOT_FOUND", `API path ${routeData.path} not implemented ${functionName}`);
|
|
2045
2914
|
await customFunction(req, res, routeData);
|
|
2046
2915
|
}
|
|
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
2916
|
async storeFileSystemSchema() {
|
|
2061
2917
|
const schemaPrettyStr = await prettier3.format(JSON.stringify(this.schema), __spreadValues({
|
|
2062
2918
|
parser: "json"
|
|
@@ -2068,7 +2924,7 @@ var ResturaEngine = class {
|
|
|
2068
2924
|
printWidth: 120,
|
|
2069
2925
|
singleQuote: true
|
|
2070
2926
|
}));
|
|
2071
|
-
|
|
2927
|
+
fs4.writeFileSync(this.resturaConfig.schemaFilePath, schemaPrettyStr);
|
|
2072
2928
|
}
|
|
2073
2929
|
resetPublicEndpoints() {
|
|
2074
2930
|
this.publicEndpoints = {
|
|
@@ -2085,13 +2941,13 @@ var ResturaEngine = class {
|
|
|
2085
2941
|
if (!routeData.roles.includes(role))
|
|
2086
2942
|
throw new RsError("UNAUTHORIZED", "Not authorized to access this endpoint");
|
|
2087
2943
|
}
|
|
2088
|
-
getRouteData(method, baseUrl,
|
|
2944
|
+
getRouteData(method, baseUrl, path5) {
|
|
2089
2945
|
const endpoint = this.schema.endpoints.find((item) => {
|
|
2090
2946
|
return item.baseUrl === baseUrl;
|
|
2091
2947
|
});
|
|
2092
2948
|
if (!endpoint) throw new RsError("NOT_FOUND", "Route not found");
|
|
2093
2949
|
const route = endpoint.routes.find((item) => {
|
|
2094
|
-
return item.method === method && item.path ===
|
|
2950
|
+
return item.method === method && item.path === path5;
|
|
2095
2951
|
});
|
|
2096
2952
|
if (!route) throw new RsError("NOT_FOUND", "Route not found");
|
|
2097
2953
|
return route;
|
|
@@ -2112,6 +2968,9 @@ __decorateClass([
|
|
|
2112
2968
|
__decorateClass([
|
|
2113
2969
|
boundMethod
|
|
2114
2970
|
], ResturaEngine.prototype, "getSchemaAndTypes", 1);
|
|
2971
|
+
__decorateClass([
|
|
2972
|
+
boundMethod
|
|
2973
|
+
], ResturaEngine.prototype, "getMulterFilesIfAny", 1);
|
|
2115
2974
|
__decorateClass([
|
|
2116
2975
|
boundMethod
|
|
2117
2976
|
], ResturaEngine.prototype, "executeRouteLogic", 1);
|
|
@@ -2121,24 +2980,54 @@ __decorateClass([
|
|
|
2121
2980
|
__decorateClass([
|
|
2122
2981
|
boundMethod
|
|
2123
2982
|
], 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
2983
|
var restura = new ResturaEngine();
|
|
2984
|
+
|
|
2985
|
+
// src/restura/sql/PsqlTransaction.ts
|
|
2986
|
+
import pg3 from "pg";
|
|
2987
|
+
var { Client: Client2 } = pg3;
|
|
2988
|
+
var PsqlTransaction = class extends PsqlConnection {
|
|
2989
|
+
constructor(clientConfig, instanceId) {
|
|
2990
|
+
super(instanceId);
|
|
2991
|
+
this.clientConfig = clientConfig;
|
|
2992
|
+
this.client = new Client2(clientConfig);
|
|
2993
|
+
this.connectPromise = this.client.connect();
|
|
2994
|
+
this.beginTransactionPromise = this.beginTransaction();
|
|
2995
|
+
}
|
|
2996
|
+
async close() {
|
|
2997
|
+
if (this.client) {
|
|
2998
|
+
await this.client.end();
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
async beginTransaction() {
|
|
3002
|
+
await this.connectPromise;
|
|
3003
|
+
return this.client.query("BEGIN");
|
|
3004
|
+
}
|
|
3005
|
+
async rollback() {
|
|
3006
|
+
return this.query("ROLLBACK");
|
|
3007
|
+
}
|
|
3008
|
+
async commit() {
|
|
3009
|
+
return this.query("COMMIT");
|
|
3010
|
+
}
|
|
3011
|
+
async release() {
|
|
3012
|
+
return this.client.end();
|
|
3013
|
+
}
|
|
3014
|
+
async query(query, values) {
|
|
3015
|
+
await this.connectPromise;
|
|
3016
|
+
await this.beginTransactionPromise;
|
|
3017
|
+
return this.client.query(query, values);
|
|
3018
|
+
}
|
|
3019
|
+
};
|
|
2136
3020
|
export {
|
|
2137
3021
|
HtmlStatusCodes,
|
|
3022
|
+
PsqlConnection,
|
|
3023
|
+
PsqlEngine,
|
|
2138
3024
|
PsqlPool,
|
|
3025
|
+
PsqlTransaction,
|
|
2139
3026
|
RsError,
|
|
2140
3027
|
SQL,
|
|
2141
3028
|
escapeColumnName,
|
|
3029
|
+
eventManager_default as eventManager,
|
|
3030
|
+
filterPsqlParser_default as filterPsqlParser,
|
|
2142
3031
|
insertObjectQuery,
|
|
2143
3032
|
isValueNumber2 as isValueNumber,
|
|
2144
3033
|
logger,
|