prostgles-server 4.2.159 → 4.2.161
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/Auth/setEmailProvider.js +2 -2
- package/dist/Auth/setEmailProvider.js.map +1 -1
- package/lib/Auth/AuthHandler.ts +436 -0
- package/lib/Auth/AuthTypes.ts +280 -0
- package/lib/Auth/getSafeReturnURL.ts +35 -0
- package/lib/Auth/sendEmail.ts +83 -0
- package/lib/Auth/setAuthProviders.ts +128 -0
- package/lib/Auth/setEmailProvider.ts +85 -0
- package/lib/Auth/setupAuthRoutes.ts +161 -0
- package/lib/DBEventsManager.ts +178 -0
- package/lib/DBSchemaBuilder.ts +225 -0
- package/lib/DboBuilder/DboBuilder.ts +319 -0
- package/lib/DboBuilder/DboBuilderTypes.ts +361 -0
- package/lib/DboBuilder/QueryBuilder/Functions.ts +1153 -0
- package/lib/DboBuilder/QueryBuilder/QueryBuilder.ts +288 -0
- package/lib/DboBuilder/QueryBuilder/getJoinQuery.ts +263 -0
- package/lib/DboBuilder/QueryBuilder/getNewQuery.ts +271 -0
- package/lib/DboBuilder/QueryBuilder/getSelectQuery.ts +136 -0
- package/lib/DboBuilder/QueryBuilder/prepareHaving.ts +22 -0
- package/lib/DboBuilder/QueryStreamer.ts +250 -0
- package/lib/DboBuilder/TableHandler/DataValidator.ts +428 -0
- package/lib/DboBuilder/TableHandler/TableHandler.ts +205 -0
- package/lib/DboBuilder/TableHandler/delete.ts +115 -0
- package/lib/DboBuilder/TableHandler/insert.ts +183 -0
- package/lib/DboBuilder/TableHandler/insertTest.ts +78 -0
- package/lib/DboBuilder/TableHandler/onDeleteFromFileTable.ts +62 -0
- package/lib/DboBuilder/TableHandler/runInsertUpdateQuery.ts +134 -0
- package/lib/DboBuilder/TableHandler/update.ts +126 -0
- package/lib/DboBuilder/TableHandler/updateBatch.ts +49 -0
- package/lib/DboBuilder/TableHandler/updateFile.ts +48 -0
- package/lib/DboBuilder/TableHandler/upsert.ts +34 -0
- package/lib/DboBuilder/ViewHandler/ViewHandler.ts +393 -0
- package/lib/DboBuilder/ViewHandler/count.ts +38 -0
- package/lib/DboBuilder/ViewHandler/find.ts +153 -0
- package/lib/DboBuilder/ViewHandler/getExistsCondition.ts +73 -0
- package/lib/DboBuilder/ViewHandler/getExistsFilters.ts +74 -0
- package/lib/DboBuilder/ViewHandler/getInfo.ts +32 -0
- package/lib/DboBuilder/ViewHandler/getTableJoinQuery.ts +84 -0
- package/lib/DboBuilder/ViewHandler/parseComplexFilter.ts +96 -0
- package/lib/DboBuilder/ViewHandler/parseFieldFilter.ts +105 -0
- package/lib/DboBuilder/ViewHandler/parseJoinPath.ts +208 -0
- package/lib/DboBuilder/ViewHandler/prepareSortItems.ts +163 -0
- package/lib/DboBuilder/ViewHandler/prepareWhere.ts +90 -0
- package/lib/DboBuilder/ViewHandler/size.ts +37 -0
- package/lib/DboBuilder/ViewHandler/subscribe.ts +118 -0
- package/lib/DboBuilder/ViewHandler/validateViewRules.ts +70 -0
- package/lib/DboBuilder/dboBuilderUtils.ts +222 -0
- package/lib/DboBuilder/getColumns.ts +114 -0
- package/lib/DboBuilder/getCondition.ts +201 -0
- package/lib/DboBuilder/getSubscribeRelatedTables.ts +190 -0
- package/lib/DboBuilder/getTablesForSchemaPostgresSQL.ts +426 -0
- package/lib/DboBuilder/insertNestedRecords.ts +355 -0
- package/lib/DboBuilder/parseUpdateRules.ts +187 -0
- package/lib/DboBuilder/prepareShortestJoinPaths.ts +186 -0
- package/lib/DboBuilder/runSQL.ts +182 -0
- package/lib/DboBuilder/runTransaction.ts +50 -0
- package/lib/DboBuilder/sqlErrCodeToMsg.ts +254 -0
- package/lib/DboBuilder/uploadFile.ts +69 -0
- package/lib/Event_Trigger_Tags.ts +118 -0
- package/lib/FileManager/FileManager.ts +358 -0
- package/lib/FileManager/getValidatedFileType.ts +69 -0
- package/lib/FileManager/initFileManager.ts +187 -0
- package/lib/FileManager/upload.ts +62 -0
- package/lib/FileManager/uploadStream.ts +79 -0
- package/lib/Filtering.ts +463 -0
- package/lib/JSONBValidation/validate_jsonb_schema_sql.ts +502 -0
- package/lib/JSONBValidation/validation.ts +143 -0
- package/lib/Logging.ts +127 -0
- package/lib/PostgresNotifListenManager.ts +143 -0
- package/lib/Prostgles.ts +485 -0
- package/lib/ProstglesTypes.ts +196 -0
- package/lib/PubSubManager/PubSubManager.ts +609 -0
- package/lib/PubSubManager/addSub.ts +138 -0
- package/lib/PubSubManager/addSync.ts +141 -0
- package/lib/PubSubManager/getCreatePubSubManagerError.ts +72 -0
- package/lib/PubSubManager/getPubSubManagerInitQuery.ts +662 -0
- package/lib/PubSubManager/initPubSubManager.ts +79 -0
- package/lib/PubSubManager/notifListener.ts +173 -0
- package/lib/PubSubManager/orphanTriggerCheck.ts +70 -0
- package/lib/PubSubManager/pushSubData.ts +55 -0
- package/lib/PublishParser/PublishParser.ts +162 -0
- package/lib/PublishParser/getFileTableRules.ts +124 -0
- package/lib/PublishParser/getSchemaFromPublish.ts +141 -0
- package/lib/PublishParser/getTableRulesWithoutFileTable.ts +177 -0
- package/lib/PublishParser/publishTypesAndUtils.ts +399 -0
- package/lib/RestApi.ts +127 -0
- package/lib/SchemaWatch/SchemaWatch.ts +90 -0
- package/lib/SchemaWatch/createSchemaWatchEventTrigger.ts +3 -0
- package/lib/SchemaWatch/getValidatedWatchSchemaType.ts +45 -0
- package/lib/SchemaWatch/getWatchSchemaTagList.ts +27 -0
- package/lib/SyncReplication.ts +557 -0
- package/lib/TableConfig/TableConfig.ts +468 -0
- package/lib/TableConfig/getColumnDefinitionQuery.ts +111 -0
- package/lib/TableConfig/getConstraintDefinitionQueries.ts +95 -0
- package/lib/TableConfig/getFutureTableSchema.ts +64 -0
- package/lib/TableConfig/getPGIndexes.ts +53 -0
- package/lib/TableConfig/getTableColumnQueries.ts +129 -0
- package/lib/TableConfig/initTableConfig.ts +326 -0
- package/lib/index.ts +13 -0
- package/lib/initProstgles.ts +319 -0
- package/lib/onSocketConnected.ts +102 -0
- package/lib/runClientRequest.ts +129 -0
- package/lib/shortestPath.ts +122 -0
- package/lib/typeTests/DBoGenerated.d.ts +320 -0
- package/lib/typeTests/dboTypeCheck.ts +81 -0
- package/lib/utils.ts +15 -0
- package/package.json +1 -1
package/lib/Prostgles.ts
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
/*---------------------------------------------------------------------------------------------
|
|
2
|
+
* Copyright (c) Stefan L. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License. See LICENSE in the project root for license information.
|
|
4
|
+
*--------------------------------------------------------------------------------------------*/
|
|
5
|
+
|
|
6
|
+
import * as pgPromise from 'pg-promise';
|
|
7
|
+
import { AuthHandler } from "./Auth/AuthHandler";
|
|
8
|
+
import { FileManager } from "./FileManager/FileManager";
|
|
9
|
+
import { SchemaWatch } from "./SchemaWatch/SchemaWatch";
|
|
10
|
+
import { OnInitReason, initProstgles } from "./initProstgles";
|
|
11
|
+
import { makeSocketError, onSocketConnected } from "./onSocketConnected";
|
|
12
|
+
import { clientCanRunSqlRequest, runClientSqlRequest } from "./runClientRequest";
|
|
13
|
+
import pg = require('pg-promise/typescript/pg-subset');
|
|
14
|
+
const { version } = require('../package.json');
|
|
15
|
+
|
|
16
|
+
import type { ProstglesInitOptions } from "./ProstglesTypes";
|
|
17
|
+
import { RestApi } from "./RestApi";
|
|
18
|
+
import TableConfigurator from "./TableConfig/TableConfig";
|
|
19
|
+
|
|
20
|
+
import { DBHandlerServer, DboBuilder, LocalParams, PRGLIOSocket, getErrorAsObject } from "./DboBuilder/DboBuilder";
|
|
21
|
+
export { DBHandlerServer };
|
|
22
|
+
export type PGP = pgPromise.IMain<{}, pg.IClient>;
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
CHANNELS,
|
|
26
|
+
ClientSchema,
|
|
27
|
+
DBSchemaTable,
|
|
28
|
+
SQLRequest, TableSchemaForClient,
|
|
29
|
+
isObject, omitKeys, tryCatch
|
|
30
|
+
} from "prostgles-types";
|
|
31
|
+
import { DBEventsManager } from "./DBEventsManager";
|
|
32
|
+
import { PublishParser } from "./PublishParser/PublishParser";
|
|
33
|
+
|
|
34
|
+
export type DB = pgPromise.IDatabase<{}, pg.IClient>;
|
|
35
|
+
export type DBorTx = DB | pgPromise.ITask<{}>
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
export const TABLE_METHODS = ["update", "find", "findOne", "insert", "delete", "upsert"] as const;
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
/*
|
|
43
|
+
1. Connect to db
|
|
44
|
+
2. Execute any SQL file if provided
|
|
45
|
+
3. Make DBO object from all tables and views
|
|
46
|
+
4. Set publish listeners
|
|
47
|
+
5. Finish init and provide DBO object
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
export type OnReady = {
|
|
51
|
+
dbo: DBHandlerServer;
|
|
52
|
+
db: DB;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const DEFAULT_KEYWORDS = {
|
|
56
|
+
$filter: "$filter",
|
|
57
|
+
$and: "$and",
|
|
58
|
+
$or: "$or",
|
|
59
|
+
$not: "$not"
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
import { randomUUID } from "crypto";
|
|
63
|
+
import * as fs from 'fs';
|
|
64
|
+
|
|
65
|
+
export class Prostgles {
|
|
66
|
+
/**
|
|
67
|
+
* Used facilitate concurrent prostgles connections to the same database
|
|
68
|
+
*/
|
|
69
|
+
readonly appId = randomUUID();
|
|
70
|
+
opts: ProstglesInitOptions = {
|
|
71
|
+
DEBUG_MODE: false,
|
|
72
|
+
dbConnection: {
|
|
73
|
+
host: "localhost",
|
|
74
|
+
port: 5432,
|
|
75
|
+
application_name: "prostgles_app"
|
|
76
|
+
},
|
|
77
|
+
onReady: () => {
|
|
78
|
+
//empty
|
|
79
|
+
},
|
|
80
|
+
watchSchema: false,
|
|
81
|
+
watchSchemaType: "DDL_trigger",
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
db?: DB;
|
|
85
|
+
pgp?: PGP;
|
|
86
|
+
dbo?: DBHandlerServer;
|
|
87
|
+
_dboBuilder?: DboBuilder;
|
|
88
|
+
get dboBuilder(): DboBuilder {
|
|
89
|
+
if (!this._dboBuilder) {
|
|
90
|
+
console.trace(1)
|
|
91
|
+
throw "get dboBuilder: it's undefined";
|
|
92
|
+
}
|
|
93
|
+
return this._dboBuilder;
|
|
94
|
+
}
|
|
95
|
+
set dboBuilder(d: DboBuilder) {
|
|
96
|
+
this._dboBuilder = d;
|
|
97
|
+
}
|
|
98
|
+
publishParser?: PublishParser;
|
|
99
|
+
|
|
100
|
+
authHandler?: AuthHandler;
|
|
101
|
+
|
|
102
|
+
schemaWatch?: SchemaWatch;
|
|
103
|
+
|
|
104
|
+
keywords = DEFAULT_KEYWORDS;
|
|
105
|
+
loaded = false;
|
|
106
|
+
|
|
107
|
+
dbEventsManager?: DBEventsManager;
|
|
108
|
+
schemaAge = "0";
|
|
109
|
+
|
|
110
|
+
fileManager?: FileManager;
|
|
111
|
+
restApi?: RestApi;
|
|
112
|
+
|
|
113
|
+
tableConfigurator?: TableConfigurator;
|
|
114
|
+
|
|
115
|
+
isMedia(tableName: string) {
|
|
116
|
+
return this.opts?.fileTable?.tableName === tableName;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
constructor(params: ProstglesInitOptions) {
|
|
120
|
+
if (!params) throw "ProstglesInitOptions missing";
|
|
121
|
+
|
|
122
|
+
const config: Record<keyof ProstglesInitOptions, 1> = {
|
|
123
|
+
transactions: 1, joins: 1, tsGeneratedTypesDir: 1, disableRealtime: 1,
|
|
124
|
+
onReady: 1, dbConnection: 1, dbOptions: 1, publishMethods: 1,
|
|
125
|
+
io: 1, publish: 1, schema: 1, publishRawSQL: 1, wsChannelNamePrefix: 1,
|
|
126
|
+
onSocketConnect: 1, onSocketDisconnect: 1, sqlFilePath: 1, auth: 1,
|
|
127
|
+
DEBUG_MODE: 1, watchSchema: 1, watchSchemaType: 1, fileTable: 1, onQuery: 1,
|
|
128
|
+
tableConfig: 1, tableConfigMigrations: 1, keywords: 1, onNotice: 1, onLog: 1, restApi: 1, testRulesOnConnect: 1
|
|
129
|
+
};
|
|
130
|
+
const unknownParams = Object.keys(params).filter((key: string) => !Object.keys(config).includes(key))
|
|
131
|
+
if (unknownParams.length) {
|
|
132
|
+
console.error(`Unrecognised ProstglesInitOptions params: ${unknownParams.join()}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
Object.assign(this.opts, params);
|
|
136
|
+
|
|
137
|
+
/* set defaults */
|
|
138
|
+
if (this.opts?.fileTable) {
|
|
139
|
+
this.opts.fileTable.tableName ??= "media";
|
|
140
|
+
}
|
|
141
|
+
this.opts.schema ??= { "public": 1 };
|
|
142
|
+
|
|
143
|
+
this.keywords = {
|
|
144
|
+
...DEFAULT_KEYWORDS,
|
|
145
|
+
...params.keywords,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
destroyed = false;
|
|
150
|
+
|
|
151
|
+
checkDb() {
|
|
152
|
+
if (!this.db || !this.db.connect) throw "something went wrong getting a db connection";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getTSFileName() {
|
|
156
|
+
const fileName = "DBoGenerated.d.ts" //`dbo_${this.schema}_types.ts`;
|
|
157
|
+
const _dir = (this.opts.tsGeneratedTypesDir || "");
|
|
158
|
+
const dir = _dir.endsWith("/")? _dir : `${_dir}/`;
|
|
159
|
+
const fullPath = dir + fileName;
|
|
160
|
+
return { fileName, fullPath }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private getFileText(fullPath: string, _format = "utf8"): Promise<string> {
|
|
164
|
+
return new Promise((resolve, reject) => {
|
|
165
|
+
fs.readFile(fullPath, 'utf8', function (err, data) {
|
|
166
|
+
if (err) reject(err);
|
|
167
|
+
else resolve(data);
|
|
168
|
+
});
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
getTSFileContent = () => {
|
|
173
|
+
const header = `/* This file was generated by Prostgles \n` +
|
|
174
|
+
// `* ${(new Date).toUTCString()} \n`
|
|
175
|
+
`*/ \n\n `;
|
|
176
|
+
return header + this.dboBuilder.tsTypesDefinition;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Will write the Schema Typescript definitions to file (tsGeneratedTypesDir)
|
|
181
|
+
*/
|
|
182
|
+
writeDBSchema(force = false) {
|
|
183
|
+
|
|
184
|
+
if (this.opts.tsGeneratedTypesDir) {
|
|
185
|
+
const { fullPath, fileName } = this.getTSFileName();
|
|
186
|
+
const fileContent = this.getTSFileContent();
|
|
187
|
+
fs.readFile(fullPath, 'utf8', function (err, data) {
|
|
188
|
+
if (err || (force || data !== fileContent)) {
|
|
189
|
+
fs.writeFileSync(fullPath, fileContent);
|
|
190
|
+
console.log("Prostgles: Created typescript schema definition file: \n " + fileName)
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
} else if (force) {
|
|
194
|
+
console.error("Schema changed. tsGeneratedTypesDir needs to be set to reload server")
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Will re-create the dbo object
|
|
200
|
+
*/
|
|
201
|
+
refreshDBO = async () => {
|
|
202
|
+
await this.opts.onLog?.({
|
|
203
|
+
type: "debug",
|
|
204
|
+
command: "refreshDBO.start",
|
|
205
|
+
duration: -1,
|
|
206
|
+
data: { }
|
|
207
|
+
});
|
|
208
|
+
const start = Date.now();
|
|
209
|
+
if (this._dboBuilder) {
|
|
210
|
+
await this._dboBuilder.build();
|
|
211
|
+
} else {
|
|
212
|
+
this.dboBuilder = await DboBuilder.create(this);
|
|
213
|
+
}
|
|
214
|
+
if (!this.dboBuilder) throw "this.dboBuilder";
|
|
215
|
+
this.dbo = this.dboBuilder.dbo;
|
|
216
|
+
await this.opts.onLog?.({ type: "debug", command: "refreshDBO.end", duration: Date.now() - start })
|
|
217
|
+
return this.dbo;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
initRestApi = async () => {
|
|
221
|
+
if (this.opts.restApi) {
|
|
222
|
+
this.restApi = new RestApi({ prostgles: this, ...this.opts.restApi });
|
|
223
|
+
} else {
|
|
224
|
+
this.restApi?.destroy();
|
|
225
|
+
this.restApi = undefined;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
initAuthHandler = async () => {
|
|
230
|
+
this.authHandler?.destroy();
|
|
231
|
+
this.authHandler = new AuthHandler(this as any);
|
|
232
|
+
await this.authHandler.init();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
initTableConfig = async (reason: OnInitReason) => {
|
|
236
|
+
const res = await tryCatch(async () => {
|
|
237
|
+
|
|
238
|
+
if(this.tableConfigurator?.initialising){
|
|
239
|
+
console.error("TableConfigurator WILL deadlock", { reason });
|
|
240
|
+
}
|
|
241
|
+
await this.tableConfigurator?.destroy();
|
|
242
|
+
this.tableConfigurator = new TableConfigurator(this);
|
|
243
|
+
try {
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
await this.opts.onLog?.({ type: "debug", command: "tableConfigurator.init.start", duration: -1 });
|
|
246
|
+
await this.tableConfigurator.init();
|
|
247
|
+
await this.opts.onLog?.({ type: "debug", command: "tableConfigurator.init.end", duration: Date.now() - now });
|
|
248
|
+
} catch (e) {
|
|
249
|
+
if(this.opts.tableConfigMigrations?.silentFail === false){
|
|
250
|
+
console.error("TableConfigurator silentFail: ", e);
|
|
251
|
+
} else {
|
|
252
|
+
throw e;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
await this.opts.onLog?.({ type: "debug", command: "initTableConfig", ...res });
|
|
257
|
+
if(res.hasError) throw res.error;
|
|
258
|
+
return res.data;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* Create media table if required */
|
|
262
|
+
initFileTable = async () => {
|
|
263
|
+
const res = await tryCatch(async () => {
|
|
264
|
+
|
|
265
|
+
if (this.opts.fileTable) {
|
|
266
|
+
const { cloudClient, localConfig, imageOptions } = this.opts.fileTable;
|
|
267
|
+
await this.refreshDBO();
|
|
268
|
+
if (!cloudClient && !localConfig) throw "fileTable missing param: Must provide awsS3Config OR localConfig";
|
|
269
|
+
|
|
270
|
+
this.fileManager = new FileManager(cloudClient || localConfig!, imageOptions);
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
await this.fileManager.init(this);
|
|
274
|
+
} catch (e) {
|
|
275
|
+
console.error("FileManager: ", e);
|
|
276
|
+
this.fileManager = undefined;
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
await this.fileManager?.destroy();
|
|
280
|
+
this.fileManager = undefined;
|
|
281
|
+
}
|
|
282
|
+
await this.refreshDBO();
|
|
283
|
+
return { data: {} }
|
|
284
|
+
});
|
|
285
|
+
await this.opts.onLog?.({
|
|
286
|
+
type: "debug",
|
|
287
|
+
command: "initFileTable",
|
|
288
|
+
...res,
|
|
289
|
+
});
|
|
290
|
+
if(res.error !== undefined) throw res.error;
|
|
291
|
+
return res.data;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
isSuperUser = false;
|
|
295
|
+
|
|
296
|
+
init = initProstgles.bind(this);
|
|
297
|
+
|
|
298
|
+
async runSQLFile(filePath: string) {
|
|
299
|
+
|
|
300
|
+
const res = await tryCatch(async () => {
|
|
301
|
+
const fileContent = await this.getFileText(filePath);//.then(console.log);
|
|
302
|
+
|
|
303
|
+
const result = await this.db?.multi(fileContent)
|
|
304
|
+
.then((data) => {
|
|
305
|
+
console.log("Prostgles: SQL file executed successfuly \n -> " + filePath);
|
|
306
|
+
return data;
|
|
307
|
+
}).catch((err) => {
|
|
308
|
+
const { position, length } = err,
|
|
309
|
+
lines = fileContent.split("\n");
|
|
310
|
+
let errMsg = filePath + " error: ";
|
|
311
|
+
|
|
312
|
+
if (position && length && fileContent) {
|
|
313
|
+
const startLine = Math.max(0, fileContent.substring(0, position).split("\n").length - 2),
|
|
314
|
+
endLine = startLine + 3;
|
|
315
|
+
|
|
316
|
+
errMsg += "\n\n";
|
|
317
|
+
errMsg += lines.slice(startLine, endLine).map((txt, i) => `${startLine + i + 1} ${i === 1 ? "->" : " "} ${txt}`).join("\n");
|
|
318
|
+
errMsg += "\n\n";
|
|
319
|
+
}
|
|
320
|
+
console.error(errMsg, err);
|
|
321
|
+
return Promise.reject(err);
|
|
322
|
+
});
|
|
323
|
+
return { success: result?.length }
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
await this.opts.onLog?.({ type: "debug", command: "runSQLFile", ...res });
|
|
327
|
+
if(res.error !== undefined) throw res.error;
|
|
328
|
+
return res.success;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
connectedSockets: PRGLIOSocket[] = [];
|
|
333
|
+
async setSocketEvents() {
|
|
334
|
+
this.checkDb();
|
|
335
|
+
|
|
336
|
+
if (!this.dbo) throw "dbo missing";
|
|
337
|
+
|
|
338
|
+
const publishParser = new PublishParser(
|
|
339
|
+
this.opts.publish,
|
|
340
|
+
this.opts.publishMethods,
|
|
341
|
+
this.opts.publishRawSQL,
|
|
342
|
+
this.dbo,
|
|
343
|
+
this.db!,
|
|
344
|
+
this
|
|
345
|
+
);
|
|
346
|
+
this.publishParser = publishParser;
|
|
347
|
+
|
|
348
|
+
if (!this.opts.io) return;
|
|
349
|
+
|
|
350
|
+
/* Already initialised. Only reconnect sockets */
|
|
351
|
+
if (this.connectedSockets.length) {
|
|
352
|
+
this.connectedSockets.forEach(s => {
|
|
353
|
+
s.emit(CHANNELS.SCHEMA_CHANGED);
|
|
354
|
+
this.pushSocketSchema(s);
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* Initialise */
|
|
360
|
+
this.opts.io.removeAllListeners('connection');
|
|
361
|
+
this.opts.io.on('connection', this.onSocketConnected);
|
|
362
|
+
/** In some cases io will re-init with already connected sockets */
|
|
363
|
+
this.opts.io?.sockets.sockets.forEach(socket => this.onSocketConnected(socket))
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
onSocketConnected = onSocketConnected.bind(this);
|
|
367
|
+
|
|
368
|
+
getClientSchema = async (clientReq: Pick<LocalParams, "socket" | "httpReq">) => {
|
|
369
|
+
|
|
370
|
+
const result = await tryCatch(async () => {
|
|
371
|
+
|
|
372
|
+
const clientInfo = clientReq.socket? { type: "socket" as const, socket: clientReq.socket } : clientReq.httpReq? { type: "http" as const, httpReq: clientReq.httpReq } : undefined;
|
|
373
|
+
if(!clientInfo) throw "Invalid client";
|
|
374
|
+
if(!this.authHandler) throw "this.authHandler missing";
|
|
375
|
+
const userData = await this.authHandler.getClientInfo(clientInfo);
|
|
376
|
+
const { publishParser } = this;
|
|
377
|
+
let fullSchema: {
|
|
378
|
+
schema: TableSchemaForClient;
|
|
379
|
+
tables: DBSchemaTable[];
|
|
380
|
+
} | undefined;
|
|
381
|
+
let publishValidationError;
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
if (!publishParser) throw "publishParser undefined";
|
|
385
|
+
fullSchema = await publishParser.getSchemaFromPublish({ ...clientInfo, userData });
|
|
386
|
+
} catch (e) {
|
|
387
|
+
publishValidationError = e;
|
|
388
|
+
console.error(`\nProstgles Publish validation failed (after socket connected):\n ->`, e);
|
|
389
|
+
}
|
|
390
|
+
let rawSQL = false;
|
|
391
|
+
if (this.opts.publishRawSQL && typeof this.opts.publishRawSQL === "function") {
|
|
392
|
+
const { allowed } = await clientCanRunSqlRequest.bind(this)(clientInfo);
|
|
393
|
+
rawSQL = allowed;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const { schema, tables } = fullSchema ?? { schema: {}, tables: [] };
|
|
397
|
+
const joinTables2: string[][] = [];
|
|
398
|
+
if (this.opts.joins) {
|
|
399
|
+
const _joinTables2 = this.dboBuilder.getAllJoinPaths()
|
|
400
|
+
.filter(jp =>
|
|
401
|
+
![jp.t1, jp.t2].find(t => !schema[t] || !schema[t]?.findOne)
|
|
402
|
+
).map(jp => [jp.t1, jp.t2].sort());
|
|
403
|
+
_joinTables2.map(jt => {
|
|
404
|
+
if (!joinTables2.find(_jt => _jt.join() === jt.join())) {
|
|
405
|
+
joinTables2.push(jt);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const methods = await publishParser?.getAllowedMethods(clientInfo, userData);
|
|
411
|
+
|
|
412
|
+
const methodSchema: ClientSchema["methods"] = !methods? [] : Object.entries(methods).map(([methodName, method]) => {
|
|
413
|
+
if(isObject(method) && "run" in method){
|
|
414
|
+
return {
|
|
415
|
+
name: methodName,
|
|
416
|
+
...omitKeys(method, ["run"]),
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return methodName;
|
|
420
|
+
}).sort((a, b) => {
|
|
421
|
+
const aName = isObject(a)? a.name : a;
|
|
422
|
+
const bName = isObject(b)? b.name : b;
|
|
423
|
+
return aName.localeCompare(bName);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const { auth } = await this.authHandler.getClientAuth(clientReq);
|
|
427
|
+
|
|
428
|
+
const clientSchema: ClientSchema = {
|
|
429
|
+
schema,
|
|
430
|
+
methods: methodSchema,
|
|
431
|
+
tableSchema: tables,
|
|
432
|
+
rawSQL,
|
|
433
|
+
joinTables: joinTables2,
|
|
434
|
+
auth,
|
|
435
|
+
version,
|
|
436
|
+
err: publishValidationError? "Server Error: User publish validation failed." : undefined
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
publishValidationError,
|
|
441
|
+
clientSchema,
|
|
442
|
+
userData
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
const sid = result.userData?.sid ?? this.authHandler?.getSIDNoError(clientReq);
|
|
446
|
+
await this.opts.onLog?.({
|
|
447
|
+
type: "connect.getClientSchema",
|
|
448
|
+
duration: result.duration,
|
|
449
|
+
sid,
|
|
450
|
+
socketId: clientReq.socket?.id,
|
|
451
|
+
error: result.error || result.publishValidationError,
|
|
452
|
+
});
|
|
453
|
+
if(result.hasError) throw result.error;
|
|
454
|
+
return result.clientSchema;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
pushSocketSchema = async (socket: PRGLIOSocket) => {
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
const clientSchema = await this.getClientSchema({ socket });
|
|
461
|
+
socket.prostgles = socket.prostgles || {};
|
|
462
|
+
socket.prostgles.schema = clientSchema.schema;
|
|
463
|
+
if (clientSchema.rawSQL) {
|
|
464
|
+
socket.removeAllListeners(CHANNELS.SQL)
|
|
465
|
+
socket.on(CHANNELS.SQL, async ({ query, params, options }: SQLRequest, cb = (..._callback: any) => { /* Empty */ }) => {
|
|
466
|
+
|
|
467
|
+
runClientSqlRequest.bind(this)({ type: "socket", socket, query, args: params, options }).then(res => {
|
|
468
|
+
cb(null, res);
|
|
469
|
+
}).catch(err => {
|
|
470
|
+
makeSocketError(cb, err);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
socket.emit(CHANNELS.SCHEMA, clientSchema);
|
|
475
|
+
|
|
476
|
+
} catch (err) {
|
|
477
|
+
socket.emit(CHANNELS.SCHEMA, { err: getErrorAsObject(err) });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
export async function getIsSuperUser(db: DB): Promise<boolean> {
|
|
484
|
+
return db.oneOrNone("select usesuper from pg_user where usename = CURRENT_USER;").then(r => r.usesuper);
|
|
485
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
|
|
2
|
+
import { FileColumnConfig } from "prostgles-types";
|
|
3
|
+
import { Auth, AuthRequestParams, SessionUser } from "./Auth/AuthTypes";
|
|
4
|
+
import { EventTriggerTagFilter } from "./Event_Trigger_Tags";
|
|
5
|
+
import { CloudClient, ImageOptions, LocalConfig } from "./FileManager/FileManager";
|
|
6
|
+
import { EventInfo } from "./Logging";
|
|
7
|
+
import { ExpressApp } from "./RestApi";
|
|
8
|
+
import { OnSchemaChangeCallback } from "./SchemaWatch/SchemaWatch";
|
|
9
|
+
import { ColConstraint } from "./TableConfig/getConstraintDefinitionQueries";
|
|
10
|
+
import { DbConnection, DbConnectionOpts, OnReadyCallback } from "./initProstgles";
|
|
11
|
+
import { RestApiConfig } from "./RestApi";
|
|
12
|
+
import { TableConfig } from "./TableConfig/TableConfig";
|
|
13
|
+
|
|
14
|
+
import { PRGLIOSocket } from "./DboBuilder/DboBuilder";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
AnyObject
|
|
18
|
+
} from "prostgles-types";
|
|
19
|
+
import type { Server } from "socket.io";
|
|
20
|
+
import { Publish, PublishMethods, PublishParams } from "./PublishParser/PublishParser";
|
|
21
|
+
import { DB } from "./Prostgles";
|
|
22
|
+
import pgPromise from "pg-promise";
|
|
23
|
+
import pg from "pg-promise/typescript/pg-subset";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Allows uploading and downloading files.
|
|
27
|
+
* Currently supports only S3.
|
|
28
|
+
*
|
|
29
|
+
* @description
|
|
30
|
+
* Will create a media table that contains file metadata and urls
|
|
31
|
+
* Inserting a file into this table through prostgles will upload it to S3 and insert the relevant metadata into the media table
|
|
32
|
+
* Requesting a file from HTTP GET {fileUrlPath}/{fileId} will:
|
|
33
|
+
* 1. check auth (if provided)
|
|
34
|
+
* 2. check the permissions in publish (if provided)
|
|
35
|
+
* 3. redirect the request to the signed url (if allowed)
|
|
36
|
+
*
|
|
37
|
+
* Specifying referencedTables will:
|
|
38
|
+
* 1. create a column in that table called media
|
|
39
|
+
* 2. create a lookup table lookup_media_{referencedTable} that joins referencedTable to the media table
|
|
40
|
+
*/
|
|
41
|
+
export type FileTableConfig = {
|
|
42
|
+
tableName?: string; /* defaults to 'media' */
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* GET path used in serving media. defaults to /${tableName}
|
|
46
|
+
*/
|
|
47
|
+
fileServeRoute?: string;
|
|
48
|
+
|
|
49
|
+
cloudClient?: CloudClient;
|
|
50
|
+
localConfig?: LocalConfig;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* If defined the the files will not be deleted immediately
|
|
54
|
+
* Instead, the "deleted" field will be updated to the current timestamp and after the day interval provided in "deleteAfterNDays" the files will be deleted
|
|
55
|
+
* "checkIntervalMinutes" is the frequency in hours at which the files ready for deletion are deleted
|
|
56
|
+
*/
|
|
57
|
+
delayedDelete?: {
|
|
58
|
+
/**
|
|
59
|
+
* Minimum amount of time measured in days for which the files will not be deleted after requesting delete
|
|
60
|
+
*/
|
|
61
|
+
deleteAfterNDays: number;
|
|
62
|
+
/**
|
|
63
|
+
* How freuquently the files will be checked for deletion delay
|
|
64
|
+
*/
|
|
65
|
+
checkIntervalHours?: number;
|
|
66
|
+
}
|
|
67
|
+
expressApp: ExpressApp;
|
|
68
|
+
referencedTables?: {
|
|
69
|
+
[tableName: string]:
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* If defined then will try to create (if necessary) these columns which will reference files_table(id)
|
|
73
|
+
* Prostgles UI will use these hints (obtained through tableHandler.getInfo())
|
|
74
|
+
* */
|
|
75
|
+
| { type: "column", referenceColumns: Record<string, FileColumnConfig> }
|
|
76
|
+
},
|
|
77
|
+
imageOptions?: ImageOptions
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
type Keywords = {
|
|
83
|
+
$and: string;
|
|
84
|
+
$or: string;
|
|
85
|
+
$not: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const JOIN_TYPES = ["one-many", "many-one", "one-one", "many-many"] as const;
|
|
89
|
+
export type Join = {
|
|
90
|
+
tables: [string, string];
|
|
91
|
+
on: { [key: string]: string }[]; // Allow multi references to table
|
|
92
|
+
type: typeof JOIN_TYPES[number];
|
|
93
|
+
};
|
|
94
|
+
type Joins = Join[] | "inferred";
|
|
95
|
+
|
|
96
|
+
export type ProstglesInitOptions<S = void, SUser extends SessionUser = SessionUser> = {
|
|
97
|
+
dbConnection: DbConnection;
|
|
98
|
+
dbOptions?: DbConnectionOpts;
|
|
99
|
+
tsGeneratedTypesDir?: string;
|
|
100
|
+
disableRealtime?: boolean;
|
|
101
|
+
io?: Server;
|
|
102
|
+
publish?: Publish<S, SUser>;
|
|
103
|
+
/**
|
|
104
|
+
* If true then will test all table methods on each socket connect
|
|
105
|
+
*/
|
|
106
|
+
testRulesOnConnect?: boolean;
|
|
107
|
+
publishMethods?: PublishMethods<S, SUser>;
|
|
108
|
+
publishRawSQL?(params: PublishParams<S, SUser>): ((boolean | "*") | Promise<(boolean | "*")>);
|
|
109
|
+
joins?: Joins;
|
|
110
|
+
schema?: Record<string, 1> | Record<string, 0>;
|
|
111
|
+
sqlFilePath?: string;
|
|
112
|
+
onReady: OnReadyCallback<S>;
|
|
113
|
+
transactions?: string | boolean;
|
|
114
|
+
wsChannelNamePrefix?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Use for connection verification. Will disconnect socket on any errors
|
|
117
|
+
*/
|
|
118
|
+
onSocketConnect?: (args: AuthRequestParams<S, SUser> & { socket: PRGLIOSocket }) => void | Promise<void>;
|
|
119
|
+
onSocketDisconnect?: (args: AuthRequestParams<S, SUser> & { socket: PRGLIOSocket }) => void | Promise<void>;
|
|
120
|
+
auth?: Auth<S, SUser>;
|
|
121
|
+
DEBUG_MODE?: boolean;
|
|
122
|
+
onQuery?: (error: any, ctx: pgPromise.IEventContext<pg.IClient>) => void;
|
|
123
|
+
watchSchemaType?:
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Will set database event trigger for schema changes. Requires superuser
|
|
127
|
+
* Default
|
|
128
|
+
*/
|
|
129
|
+
| "DDL_trigger"
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Will check client queries for schema changes
|
|
133
|
+
* fallback if DDL not possible
|
|
134
|
+
*/
|
|
135
|
+
| "prostgles_queries"
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* If truthy then DBoGenerated.d.ts will be updated and "onReady" will be called with new schema on both client and server
|
|
139
|
+
*/
|
|
140
|
+
watchSchema?:
|
|
141
|
+
/**
|
|
142
|
+
* Will listen only to few events (create table/view)
|
|
143
|
+
*/
|
|
144
|
+
| boolean
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Will listen to specified events (or all if "*" is specified)
|
|
148
|
+
*/
|
|
149
|
+
| EventTriggerTagFilter
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Will only rewrite the DBoGenerated.d.ts found in tsGeneratedTypesDir
|
|
153
|
+
* This is meant to be used in development when server restarts on file change
|
|
154
|
+
*/
|
|
155
|
+
| "hotReloadMode"
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Function called when schema changes. Nothing else triggered
|
|
159
|
+
*/
|
|
160
|
+
| OnSchemaChangeCallback;
|
|
161
|
+
|
|
162
|
+
keywords?: Keywords;
|
|
163
|
+
onNotice?: (notice: AnyObject, message?: string) => void;
|
|
164
|
+
fileTable?: FileTableConfig;
|
|
165
|
+
restApi?: RestApiConfig;
|
|
166
|
+
/**
|
|
167
|
+
* Creates tables and provides UI labels, autocomplete and hints for a given json structure
|
|
168
|
+
*/
|
|
169
|
+
tableConfig?: TableConfig;
|
|
170
|
+
tableConfigMigrations?: {
|
|
171
|
+
/**
|
|
172
|
+
* If false then prostgles won't start on any tableConfig error
|
|
173
|
+
* true by default
|
|
174
|
+
*/
|
|
175
|
+
silentFail?: boolean;
|
|
176
|
+
|
|
177
|
+
version: number;
|
|
178
|
+
/** Table that will contain the schema version number and the tableConfig
|
|
179
|
+
* Defaults to schema_version
|
|
180
|
+
*/
|
|
181
|
+
versionTableName?: string;
|
|
182
|
+
/**
|
|
183
|
+
* Script run before tableConfig is loaded IF an older schema_version is present
|
|
184
|
+
*/
|
|
185
|
+
onMigrate: (args: {
|
|
186
|
+
db: DB;
|
|
187
|
+
oldVersion: number | undefined;
|
|
188
|
+
getConstraints: (
|
|
189
|
+
table: string,
|
|
190
|
+
column?: string,
|
|
191
|
+
types?: ColConstraint["type"][]
|
|
192
|
+
) => Promise<ColConstraint[]>
|
|
193
|
+
}) => void;
|
|
194
|
+
};
|
|
195
|
+
onLog?: (evt: EventInfo) => Promise<void>;
|
|
196
|
+
}
|