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
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { DATA_TYPES } from "prostgles-types";
|
|
2
|
+
import { PubSubManager } from "../PubSubManager/PubSubManager";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const raiseException = (err: string) => `
|
|
6
|
+
IF (context->'silent')::BOOLEAN = TRUE THEN
|
|
7
|
+
RETURN FALSE;
|
|
8
|
+
ELSE
|
|
9
|
+
RAISE EXCEPTION ${err} USING HINT = path, COLUMN = colname, TABLE = tablename, CONSTRAINT = 'validate_jsonb_schema: ' || jsonb_pretty(jsonb_schema::JSONB);
|
|
10
|
+
END IF;
|
|
11
|
+
`
|
|
12
|
+
|
|
13
|
+
export const VALIDATE_SCHEMA_FUNCNAME = "validate_jsonb_schema" as const;
|
|
14
|
+
export const JSONB_DATA_TYPES = [
|
|
15
|
+
...DATA_TYPES,
|
|
16
|
+
"Lookup","Lookup[]"
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
export const validate_jsonb_schema_sql = `
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* ${PubSubManager.EXCLUDE_QUERY_FROM_SCHEMA_WATCH_ID}
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
CREATE OR REPLACE FUNCTION ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
26
|
+
jsonb_schema TEXT,
|
|
27
|
+
data JSONB,
|
|
28
|
+
context JSONB DEFAULT '{}'::JSONB, /* { table: string; column: string; silent: boolean } */
|
|
29
|
+
checked_path TEXT[] DEFAULT ARRAY[]::TEXT[]
|
|
30
|
+
) RETURNS boolean AS
|
|
31
|
+
$f$
|
|
32
|
+
DECLARE
|
|
33
|
+
sub_schema RECORD;
|
|
34
|
+
array_element RECORD;
|
|
35
|
+
obj_key_val RECORD;
|
|
36
|
+
schema JSONB;
|
|
37
|
+
path text;
|
|
38
|
+
allowed_types text[] = '{${JSONB_DATA_TYPES.join(",")}}';
|
|
39
|
+
typeStr TEXT = NULL;
|
|
40
|
+
optional boolean;
|
|
41
|
+
nullable boolean;
|
|
42
|
+
colname TEXT = COALESCE(context->>'column', '');
|
|
43
|
+
tablename TEXT = COALESCE(context->>'table', '');
|
|
44
|
+
oneof JSONB;
|
|
45
|
+
arrayof JSONB;
|
|
46
|
+
lookup_data_def_schema TEXT = $d$
|
|
47
|
+
{
|
|
48
|
+
"type": { "enum": ["data", "data-def"] },
|
|
49
|
+
"table": "string",
|
|
50
|
+
"column": "string",
|
|
51
|
+
"lookup": { "type": "any", "optional": true },
|
|
52
|
+
"isArray": { "type": "boolean", "optional": true },
|
|
53
|
+
"filter": { "optional": true, "type": "any" },
|
|
54
|
+
"isFullRow": { "optional": true, "type": {
|
|
55
|
+
"displayColumns": { "optional": true, "type": "string[]" }
|
|
56
|
+
}},
|
|
57
|
+
"searchColumns": { "optional": true, "type": "string[]" },
|
|
58
|
+
"showInRowCard": { "optional": true, "type": "any" }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
$d$;
|
|
62
|
+
lookup_schema_schema TEXT = $d$
|
|
63
|
+
{
|
|
64
|
+
"type": { "enum": ["schema"] },
|
|
65
|
+
"object": { "enum": ["table", "column"] },
|
|
66
|
+
"isArray": { "type": "boolean", "optional": true },
|
|
67
|
+
"lookup": { "type": "any", "optional": true },
|
|
68
|
+
"filter": { "optional": true, "type": "any" }
|
|
69
|
+
}
|
|
70
|
+
$d$;
|
|
71
|
+
|
|
72
|
+
extra_keys TEXT[];
|
|
73
|
+
|
|
74
|
+
/* Used for oneOf schema errors */
|
|
75
|
+
v_state TEXT;
|
|
76
|
+
v_msg TEXT;
|
|
77
|
+
v_detail TEXT;
|
|
78
|
+
v_hint TEXT;
|
|
79
|
+
v_context TEXT;
|
|
80
|
+
v_one_of_errors TEXT;
|
|
81
|
+
|
|
82
|
+
BEGIN
|
|
83
|
+
path = concat_ws(', ',
|
|
84
|
+
'Path: ' || array_to_string(checked_path, '.'),
|
|
85
|
+
'Data: ' || data::TEXT,
|
|
86
|
+
'JSONBSchema: ' || schema::TEXT
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
IF length(jsonb_schema) = 0 THEN
|
|
90
|
+
${raiseException(`'Empty schema. %', path`)}
|
|
91
|
+
END IF;
|
|
92
|
+
|
|
93
|
+
/* Sometimes text comes double quoted from jsonb, e.g.: '"string"' */
|
|
94
|
+
jsonb_schema = CASE WHEN jsonb_schema::text ilike '"%"' THEN LEFT(RIGHT(jsonb_schema::text, -1), -1) ELSE jsonb_schema END;
|
|
95
|
+
|
|
96
|
+
/* 'string' */
|
|
97
|
+
IF ARRAY[jsonb_schema] <@ allowed_types THEN
|
|
98
|
+
schema = jsonb_build_object('type', jsonb_schema);
|
|
99
|
+
/* { "type": ... } */
|
|
100
|
+
ELSIF BTRIM(replace(jsonb_schema,E'\n','')) ILIKE '{%' THEN
|
|
101
|
+
schema = jsonb_schema::JSONB;
|
|
102
|
+
ELSE
|
|
103
|
+
${raiseException(`$$Invalid schema. Expecting 'typename' or { "type": "typename" } but received: %, %$$, jsonb_schema, path`)}
|
|
104
|
+
END IF;
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
nullable = COALESCE((schema->>'nullable')::BOOLEAN, FALSE);
|
|
108
|
+
IF data IS NULL OR jsonb_typeof(data) = 'null' THEN
|
|
109
|
+
IF NOT nullable THEN
|
|
110
|
+
${raiseException(`'Is not nullable. %', path`)}
|
|
111
|
+
ELSE
|
|
112
|
+
RETURN true;
|
|
113
|
+
END IF;
|
|
114
|
+
END IF;
|
|
115
|
+
|
|
116
|
+
IF schema ? 'enum' THEN
|
|
117
|
+
IF
|
|
118
|
+
jsonb_typeof(schema->'enum') != 'array' OR
|
|
119
|
+
jsonb_array_length(schema->'enum') < 1
|
|
120
|
+
THEN
|
|
121
|
+
${raiseException(`'Invalid schema enum (%) .Must be a non empty array %', schema->'enum', path`)}
|
|
122
|
+
END IF;
|
|
123
|
+
|
|
124
|
+
IF NOT jsonb_build_array(data) <@ (schema->'enum') THEN
|
|
125
|
+
${raiseException(`'Data not in allowed enum list (%), %', schema->'enum', path`)}
|
|
126
|
+
END IF;
|
|
127
|
+
|
|
128
|
+
ELSIF schema ? 'lookup' THEN
|
|
129
|
+
|
|
130
|
+
/* TODO: Finish validating data-def */
|
|
131
|
+
IF (schema->'lookup'->>'type' = 'data-def') THEN
|
|
132
|
+
RETURN TRUE;
|
|
133
|
+
END IF;
|
|
134
|
+
|
|
135
|
+
/* Validate lookup schema */
|
|
136
|
+
IF NOT ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
137
|
+
'{ "oneOfType": [' || concat_ws(',',lookup_data_def_schema, lookup_schema_schema) || '] }',
|
|
138
|
+
schema->'lookup',
|
|
139
|
+
context,
|
|
140
|
+
checked_path || '.schema'::TEXT
|
|
141
|
+
) THEN
|
|
142
|
+
|
|
143
|
+
RETURN FALSE;
|
|
144
|
+
END IF;
|
|
145
|
+
|
|
146
|
+
RETURN ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
147
|
+
CASE WHEN schema->'lookup'->>'type' = 'data-def' THEN
|
|
148
|
+
lookup_data_def_schema
|
|
149
|
+
WHEN schema->'lookup'->>'type' = 'schema' THEN
|
|
150
|
+
(
|
|
151
|
+
CASE WHEN schema->'lookup'->>'object' = 'table' THEN
|
|
152
|
+
'string' || (CASE WHEN (schema->'lookup'->'isArray')::BOOLEAN THEN '[]' ELSE '' END)
|
|
153
|
+
ELSE
|
|
154
|
+
'{ "type": { "table": "string", "column": "string' || (CASE WHEN (schema->'lookup'->'isArray')::BOOLEAN THEN '[]' ELSE '' END) || '" } }'
|
|
155
|
+
END
|
|
156
|
+
)
|
|
157
|
+
ELSE
|
|
158
|
+
(CASE WHEN (schema->'lookup'->'isArray')::BOOLEAN THEN 'any[]' ELSE 'any' END)
|
|
159
|
+
END,
|
|
160
|
+
data,
|
|
161
|
+
context,
|
|
162
|
+
checked_path
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
ELSIF schema ? 'type' THEN
|
|
166
|
+
|
|
167
|
+
IF jsonb_typeof(schema->'type') = 'string' THEN
|
|
168
|
+
typeStr = schema->>'type';
|
|
169
|
+
IF NOT ARRAY[typeStr] <@ allowed_types THEN
|
|
170
|
+
${raiseException(`'Bad schema type "%", allowed types: %. %',typeStr, allowed_types, path`)}
|
|
171
|
+
END IF;
|
|
172
|
+
|
|
173
|
+
/** Primitive array */
|
|
174
|
+
IF typeStr LIKE '%[]' THEN
|
|
175
|
+
|
|
176
|
+
typeStr = left(typeStr, -2);
|
|
177
|
+
|
|
178
|
+
IF jsonb_typeof(data) != 'array' THEN
|
|
179
|
+
${raiseException(`'Types not matching. Expecting an array. %', path`)}
|
|
180
|
+
END IF;
|
|
181
|
+
|
|
182
|
+
FOR array_element IN
|
|
183
|
+
SELECT value, row_number() OVER() -1 as idx
|
|
184
|
+
FROM jsonb_array_elements(data)
|
|
185
|
+
LOOP
|
|
186
|
+
IF NOT ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
187
|
+
CASE WHEN schema->'allowedValues' IS NOT NULL THEN
|
|
188
|
+
jsonb_build_object('type', typeStr, 'allowedValues', schema->'allowedValues')::TEXT
|
|
189
|
+
ELSE typeStr END,
|
|
190
|
+
array_element.value,
|
|
191
|
+
context,
|
|
192
|
+
checked_path || array_element.idx::TEXT
|
|
193
|
+
) THEN
|
|
194
|
+
|
|
195
|
+
RETURN FALSE;
|
|
196
|
+
END IF;
|
|
197
|
+
END LOOP;
|
|
198
|
+
|
|
199
|
+
RETURN TRUE;
|
|
200
|
+
|
|
201
|
+
/** Primitive */
|
|
202
|
+
ELSE
|
|
203
|
+
|
|
204
|
+
IF (
|
|
205
|
+
typeStr = 'number' AND jsonb_typeof(data) != typeStr OR
|
|
206
|
+
(typeStr = 'integer' AND (jsonb_typeof(data) != 'number' OR ceil(data::NUMERIC) != floor(data::NUMERIC))) OR
|
|
207
|
+
typeStr = 'boolean' AND jsonb_typeof(data) != typeStr OR
|
|
208
|
+
typeStr = 'string' AND jsonb_typeof(data) != typeStr OR
|
|
209
|
+
typeStr = 'any' AND jsonb_typeof(data) = 'null'
|
|
210
|
+
) THEN
|
|
211
|
+
${raiseException(`'Data type not matching. Expected: %, Actual: %, %', typeStr, jsonb_typeof(data), path`)}
|
|
212
|
+
END IF;
|
|
213
|
+
|
|
214
|
+
IF schema ? 'allowedValues' AND NOT(jsonb_build_array(data) <@ (schema->'allowedValues')) THEN
|
|
215
|
+
IF (
|
|
216
|
+
SELECT COUNT(distinct jsonb_typeof(value))
|
|
217
|
+
FROM jsonb_array_elements(schema->'allowedValues')
|
|
218
|
+
) > 1 THEN
|
|
219
|
+
${raiseException(`'Invalid schema. schema.allowedValues (%) contains more than one data type . %', schema->>'allowedValues', path`)}
|
|
220
|
+
END IF;
|
|
221
|
+
|
|
222
|
+
IF EXISTS(
|
|
223
|
+
SELECT 1
|
|
224
|
+
FROM jsonb_array_elements(schema->'allowedValues')
|
|
225
|
+
WHERE jsonb_typeof(value) != jsonb_typeof(data)
|
|
226
|
+
) THEN
|
|
227
|
+
${raiseException(`'Invalid schema. schema.allowedValues (%) contains contains values not matchine the schema.type %', schema->>'allowedValues', path`)}
|
|
228
|
+
END IF;
|
|
229
|
+
|
|
230
|
+
${raiseException(`'Data not in allowedValues (%). %', schema->>'allowedValues', path`)}
|
|
231
|
+
|
|
232
|
+
END IF;
|
|
233
|
+
|
|
234
|
+
END IF;
|
|
235
|
+
|
|
236
|
+
/* Object */
|
|
237
|
+
ELSIF jsonb_typeof(schema->'type') = 'object' THEN
|
|
238
|
+
|
|
239
|
+
IF jsonb_typeof(data) != 'object' THEN
|
|
240
|
+
${raiseException(`E'Expecting an object: \n %', path`)}
|
|
241
|
+
END IF;
|
|
242
|
+
|
|
243
|
+
extra_keys = ARRAY(SELECT k FROM (
|
|
244
|
+
SELECT jsonb_object_keys(data) as k
|
|
245
|
+
EXCEPT
|
|
246
|
+
SELECT jsonb_object_keys(schema->'type') as k
|
|
247
|
+
) t);
|
|
248
|
+
|
|
249
|
+
IF array_length(extra_keys, 1) > 0 THEN
|
|
250
|
+
${raiseException(`E'Object contains % invalid keys: [ % ] \n %', array_length(extra_keys, 1)::TEXT, array_to_string(extra_keys, ', '), path`)}
|
|
251
|
+
END IF;
|
|
252
|
+
|
|
253
|
+
FOR sub_schema IN
|
|
254
|
+
SELECT key, value
|
|
255
|
+
FROM jsonb_each(schema->'type')
|
|
256
|
+
LOOP
|
|
257
|
+
|
|
258
|
+
optional = COALESCE((sub_schema.value->>'optional')::BOOLEAN, FALSE);
|
|
259
|
+
IF NOT (data ? sub_schema.key) THEN
|
|
260
|
+
IF NOT optional THEN
|
|
261
|
+
${raiseException(`'Types not matching. Required property ("%") is missing. %', sub_schema.key , path`)}
|
|
262
|
+
END IF;
|
|
263
|
+
|
|
264
|
+
ELSIF NOT ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
265
|
+
-- sub_schema.value::TEXT,
|
|
266
|
+
CASE WHEN jsonb_typeof(sub_schema.value) = 'string' THEN TRIM(both '"' from sub_schema.value::TEXT) ELSE sub_schema.value::TEXT END,
|
|
267
|
+
data->sub_schema.key,
|
|
268
|
+
context,
|
|
269
|
+
checked_path || sub_schema.key
|
|
270
|
+
) THEN
|
|
271
|
+
RETURN false;
|
|
272
|
+
END IF;
|
|
273
|
+
|
|
274
|
+
END LOOP;
|
|
275
|
+
|
|
276
|
+
RETURN TRUE;
|
|
277
|
+
ELSE
|
|
278
|
+
${raiseException(`'Unexpected schema.type ( % ), %',jsonb_typeof(schema->'type'), path`)}
|
|
279
|
+
END IF;
|
|
280
|
+
|
|
281
|
+
/* oneOfType: [{ key_name: { type: "string" } }] */
|
|
282
|
+
ELSIF (schema ? 'oneOf' OR schema ? 'oneOfType') THEN
|
|
283
|
+
|
|
284
|
+
oneof = COALESCE(schema->'oneOf', schema->'oneOfType');
|
|
285
|
+
|
|
286
|
+
IF jsonb_typeof(oneof) != 'array' THEN
|
|
287
|
+
${raiseException(`'Unexpected oneOf schema. Expecting an array of objects but received: % , %', oneof::TEXT, path`)}
|
|
288
|
+
END IF;
|
|
289
|
+
|
|
290
|
+
FOR sub_schema IN
|
|
291
|
+
SELECT CASE WHEN schema ? 'oneOfType' THEN jsonb_build_object('type', value) ELSE value END as value,
|
|
292
|
+
row_number() over() - 1 as idx
|
|
293
|
+
FROM jsonb_array_elements(oneof)
|
|
294
|
+
LOOP
|
|
295
|
+
|
|
296
|
+
BEGIN
|
|
297
|
+
|
|
298
|
+
IF ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
299
|
+
sub_schema.value::TEXT,
|
|
300
|
+
data,
|
|
301
|
+
context,
|
|
302
|
+
checked_path
|
|
303
|
+
) THEN
|
|
304
|
+
RETURN true;
|
|
305
|
+
END IF;
|
|
306
|
+
|
|
307
|
+
/* Ignore exceptions in case the last schema will match */
|
|
308
|
+
EXCEPTION WHEN others THEN
|
|
309
|
+
|
|
310
|
+
GET STACKED DIAGNOSTICS
|
|
311
|
+
v_state = returned_sqlstate,
|
|
312
|
+
v_msg = message_text,
|
|
313
|
+
v_detail = pg_exception_detail,
|
|
314
|
+
v_hint = pg_exception_hint,
|
|
315
|
+
v_context = pg_exception_context;
|
|
316
|
+
|
|
317
|
+
/* Ignore duplicate errors */
|
|
318
|
+
IF v_one_of_errors IS NULL OR v_one_of_errors NOT ilike '%' || v_msg || '%' THEN
|
|
319
|
+
v_one_of_errors = concat_ws(
|
|
320
|
+
E'\n\n',
|
|
321
|
+
v_one_of_errors,
|
|
322
|
+
concat_ws(
|
|
323
|
+
', ',
|
|
324
|
+
'Schema index ' || sub_schema.idx::TEXT || ' error:',
|
|
325
|
+
'state: ' || v_state,
|
|
326
|
+
'message: ' || v_msg,
|
|
327
|
+
'detail: ' || v_detail,
|
|
328
|
+
'hint: ' || v_hint
|
|
329
|
+
-- 'context: ' || v_context
|
|
330
|
+
)
|
|
331
|
+
);
|
|
332
|
+
END IF;
|
|
333
|
+
END;
|
|
334
|
+
|
|
335
|
+
END LOOP;
|
|
336
|
+
|
|
337
|
+
${raiseException(`E'No oneOf schemas matching:\n % ), %', v_one_of_errors, path`)}
|
|
338
|
+
|
|
339
|
+
/* arrayOfType: { key_name: { type: "string" } } */
|
|
340
|
+
ELSIF (schema ? 'arrayOf' OR schema ? 'arrayOfType') THEN
|
|
341
|
+
|
|
342
|
+
arrayof = COALESCE(schema->'arrayOf', schema->'arrayOfType');
|
|
343
|
+
|
|
344
|
+
IF jsonb_typeof(data) != 'array' THEN
|
|
345
|
+
${raiseException(`'% is not an array.', path`)}
|
|
346
|
+
END IF;
|
|
347
|
+
|
|
348
|
+
FOR array_element IN
|
|
349
|
+
SELECT value, row_number() OVER() -1 as idx
|
|
350
|
+
FROM jsonb_array_elements(data)
|
|
351
|
+
LOOP
|
|
352
|
+
IF NOT ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
353
|
+
( CASE WHEN schema ? 'arrayOf'
|
|
354
|
+
THEN
|
|
355
|
+
schema->'arrayOf'
|
|
356
|
+
ELSE
|
|
357
|
+
(schema - 'arrayOfType' || jsonb_build_object('type', schema->'arrayOfType'))
|
|
358
|
+
END
|
|
359
|
+
)::TEXT,
|
|
360
|
+
array_element.value,
|
|
361
|
+
context,
|
|
362
|
+
checked_path || array_element.idx::TEXT
|
|
363
|
+
) THEN
|
|
364
|
+
RETURN false;
|
|
365
|
+
END IF;
|
|
366
|
+
END LOOP;
|
|
367
|
+
|
|
368
|
+
/* record: { keysEnum?: string[], values?: FieldType } */
|
|
369
|
+
ELSIF schema ? 'record' THEN
|
|
370
|
+
IF
|
|
371
|
+
jsonb_typeof(schema->'record') != 'object' OR
|
|
372
|
+
NOT (schema->'record') ? 'keysEnum'
|
|
373
|
+
AND NOT (schema->'record') ? 'values'
|
|
374
|
+
THEN
|
|
375
|
+
${raiseException(`'Invalid/empty record schema. Expecting a non empty record of: { keysEnum?: string[]; values?: FieldType } : %, %', schema, path`)}
|
|
376
|
+
END IF;
|
|
377
|
+
|
|
378
|
+
IF jsonb_typeof(data) != 'object' THEN
|
|
379
|
+
${raiseException(`'% is not an object.', path`)}
|
|
380
|
+
END IF;
|
|
381
|
+
|
|
382
|
+
FOR obj_key_val IN
|
|
383
|
+
SELECT jsonb_build_object('key', key, 'value', value) as obj
|
|
384
|
+
FROM jsonb_each(data)
|
|
385
|
+
LOOP
|
|
386
|
+
RETURN (CASE WHEN NOT (schema->'record') ? 'keysEnum' THEN TRUE ELSE ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
387
|
+
jsonb_build_object('enum', schema->'record'->'keysEnum')::TEXT,
|
|
388
|
+
(obj_key_val.obj)->'key',
|
|
389
|
+
context,
|
|
390
|
+
checked_path || ARRAY[(obj_key_val.obj)->>'key']
|
|
391
|
+
) END)
|
|
392
|
+
AND
|
|
393
|
+
(CASE WHEN NOT (schema->'record') ? 'values' THEN TRUE ELSE ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
394
|
+
schema->'record'->>'values',
|
|
395
|
+
(obj_key_val.obj)->'value',
|
|
396
|
+
context,
|
|
397
|
+
checked_path || ARRAY[(obj_key_val.obj)->>'key']
|
|
398
|
+
) END);
|
|
399
|
+
END LOOP;
|
|
400
|
+
|
|
401
|
+
ELSE
|
|
402
|
+
${raiseException(`'Unexpected schema: %, %', schema, path`)}
|
|
403
|
+
END IF;
|
|
404
|
+
|
|
405
|
+
RETURN true;
|
|
406
|
+
END;
|
|
407
|
+
$f$ LANGUAGE 'plpgsql' IMMUTABLE;
|
|
408
|
+
|
|
409
|
+
COMMENT ON FUNCTION ${VALIDATE_SCHEMA_FUNCNAME} /* ${PubSubManager.EXCLUDE_QUERY_FROM_SCHEMA_WATCH_ID} */
|
|
410
|
+
IS $$prostgles-server internal function used in column CHECK conditions to validate jsonb data against a column schema specified in tableConfig.
|
|
411
|
+
Example usage:
|
|
412
|
+
validate_jsonb_schema(
|
|
413
|
+
'{ "type": { "a": "number[]" } }',
|
|
414
|
+
'{ "a": [2] }'
|
|
415
|
+
)
|
|
416
|
+
$$;
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
/* TESTS */
|
|
420
|
+
|
|
421
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
422
|
+
'{ "enum": ["a", "b", 2] }',
|
|
423
|
+
'"a"'::JSONB
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
428
|
+
'{ "record": { "keysEnum": ["a", "b"] , "values": { "enum": [1, 2] } } }',
|
|
429
|
+
'{"a": 1, "b": 2 }'::JSONB
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
433
|
+
'{ "record": { "keysEnum": ["a", "b"] , "values": { "enum": [1, 2] } } }',
|
|
434
|
+
'{"a": 1 }'::JSONB
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
438
|
+
'{ "record": { "keysEnum": ["a", "b"] , "values": { "enum": [1, 2] } } }',
|
|
439
|
+
'{ }'::JSONB
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
443
|
+
'{ "record": { "keysEnum": ["a", "b"] } }',
|
|
444
|
+
'{"a": 1, "b": 2 }'::JSONB
|
|
445
|
+
);
|
|
446
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
447
|
+
'{ "enum": ["a", "b", 2] }',
|
|
448
|
+
'2'::JSONB
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
452
|
+
'{
|
|
453
|
+
"oneOfType": [
|
|
454
|
+
{ "a": "string" } ,
|
|
455
|
+
{
|
|
456
|
+
"a": {
|
|
457
|
+
"type": "boolean",
|
|
458
|
+
"allowedValues": [false],
|
|
459
|
+
"optional": true
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
]
|
|
463
|
+
}',
|
|
464
|
+
'{ "a": false }'
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
468
|
+
'{ "arrayOfType": { "a": "string", "narr": { "arrayOfType": { "a": { "type": "string", "optional": false, "nullable": true } } } } }',
|
|
469
|
+
'[{ "a": "ddd", "narr": [{ "a": null }] }]'::JSONB
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
473
|
+
'{ "type": { "a": { "type": "integer[]", "allowedValues": [2] } } }'::TEXT,
|
|
474
|
+
'{ "a": [2, 2] }'
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}(
|
|
478
|
+
'{ "type": { "a": { "type": "string[]", "allowedValues": ["2"] } } }'::TEXT,
|
|
479
|
+
'{ "a": ["2"] }'
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}('{ "type": "any"}', '{}');
|
|
483
|
+
|
|
484
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}('{ "type": { "a": { "enum": ["a"] } } }', '{ "a": "a"}');
|
|
485
|
+
|
|
486
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}('{ "arrayOfType": { "a": { "enum": ["a"] } } }', '[{ "a": "a"}]');
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}('{ "lookup": { "type": "data", "table": "tblName", "column": "colName" } }', '{}');
|
|
490
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}('{ "lookup": { "type": "data", "table": "tblName", "column": "colName", "isArray": true } }', '[{}]');
|
|
491
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}('{ "lookup": { "type": "schema", "object": "table" } }', '"tblName"'::JSONB);
|
|
492
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}('{ "lookup": { "type": "schema", "object": "table", "isArray": true } }', '["tblName"]');
|
|
493
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}('{ "lookup": { "type": "schema", "object": "column" } }', '{ "table": "tblName", "column": "colName" }');
|
|
494
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}('{ "lookup": { "type": "schema", "object": "column", "isArray": true } }', '{ "table": "tblName", "column": ["colName"] }');
|
|
495
|
+
|
|
496
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}('{ "type": "time"}', '"22:22"');
|
|
497
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}('{ "type": "Date"}', '"2222-22-22"');
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
SELECT ${VALIDATE_SCHEMA_FUNCNAME}('{ "oneOf": ["number"]}','2');
|
|
501
|
+
`;
|
|
502
|
+
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { getKeys, isEmpty, isObject, JSONB, TableSchema } from "prostgles-types";
|
|
2
|
+
import { postgresToTsType } from "../DboBuilder/DboBuilder";
|
|
3
|
+
import { asValue } from "../PubSubManager/PubSubManager";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const getFieldTypeObj = (rawFieldType: JSONB.FieldType): JSONB.FieldTypeObj => {
|
|
8
|
+
if(typeof rawFieldType === "string") return { type: rawFieldType };
|
|
9
|
+
|
|
10
|
+
return rawFieldType;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function validate<T>(obj: T, key: keyof T, rawFieldType: JSONB.FieldType): boolean {
|
|
14
|
+
let err = `The provided value for ${JSON.stringify(key)} is of invalid type. Expecting `;
|
|
15
|
+
const val = obj[key];
|
|
16
|
+
const fieldType = getFieldTypeObj(rawFieldType);
|
|
17
|
+
if ("type" in fieldType && fieldType.type) {
|
|
18
|
+
if (typeof fieldType.type !== "string") {
|
|
19
|
+
getKeys(fieldType.type).forEach(subKey => {
|
|
20
|
+
validate(val, subKey as any, (fieldType.type as JSONB.ObjectType["type"])[subKey]!)
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
err += fieldType.type;
|
|
24
|
+
if (fieldType.type === "boolean" && typeof val !== fieldType.type) throw new Error(err)
|
|
25
|
+
if (fieldType.type === "string" && typeof val !== fieldType.type) throw new Error(err)
|
|
26
|
+
if (fieldType.type === "number" && !Number.isFinite(val)) throw new Error(err)
|
|
27
|
+
if (fieldType.type === "integer" && !Number.isInteger(val)) throw new Error(err);
|
|
28
|
+
|
|
29
|
+
} else if (fieldType.enum) {
|
|
30
|
+
err += `on of: ${fieldType.enum}`;
|
|
31
|
+
if (!fieldType.enum.includes(val)) throw new Error(err)
|
|
32
|
+
}
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function validateSchema<S extends JSONB.ObjectType["type"]>(schema: S, obj: JSONB.GetObjectType<S>, objName?: string, optional = false) {
|
|
37
|
+
if ((!schema || isEmpty(schema)) && !optional) throw new Error(`Expecting ${objName} to be defined`);
|
|
38
|
+
getKeys(schema).forEach(k => validate(obj as any, k, schema[k]!));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
type ColOpts = { nullable?: boolean };
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
export function getJSONBSchemaTSTypes(schema: JSONB.JSONBSchema, colOpts: ColOpts, outerLeading = "", tables: TableSchema[]): string {
|
|
46
|
+
|
|
47
|
+
const getFieldType = (rawFieldType: JSONB.FieldType, isOneOf = false, innerLeading = "", depth = 0): string => {
|
|
48
|
+
const fieldType = getFieldTypeObj(rawFieldType);
|
|
49
|
+
const nullType = (fieldType.nullable ? `null | ` : "");
|
|
50
|
+
|
|
51
|
+
/** Primitives */
|
|
52
|
+
if (typeof fieldType?.type === "string") {
|
|
53
|
+
const correctType = fieldType.type
|
|
54
|
+
.replace("integer", "number")
|
|
55
|
+
.replace("time", "string")
|
|
56
|
+
.replace("timestamp", "string")
|
|
57
|
+
.replace("Date", "string");
|
|
58
|
+
|
|
59
|
+
if (fieldType.allowedValues && fieldType.type.endsWith("[]")) {
|
|
60
|
+
return nullType + ` (${fieldType.allowedValues.map(v => JSON.stringify(v)).join(" | ")})[]`
|
|
61
|
+
}
|
|
62
|
+
return nullType + correctType;
|
|
63
|
+
|
|
64
|
+
/** Object */
|
|
65
|
+
} else if (isObject(fieldType.type)) {
|
|
66
|
+
const addSemicolonIfMissing = (v: string) => v.trim().endsWith(";")? v : (v.trim() + ";");
|
|
67
|
+
const { type } = fieldType;
|
|
68
|
+
const spacing = isOneOf ? " " : " ";
|
|
69
|
+
let objDef = ` {${spacing}` + getKeys(type).map(key => {
|
|
70
|
+
const fieldType = getFieldTypeObj(type[key]!);
|
|
71
|
+
const escapedKey = isValidIdentifier(key) ? key : JSON.stringify(key);
|
|
72
|
+
return `${spacing}${escapedKey}${fieldType.optional ? "?" : ""}: ` + addSemicolonIfMissing(getFieldType(fieldType, true, undefined, depth + 1));
|
|
73
|
+
}).join(" ") + `${spacing}}`;
|
|
74
|
+
if(!isOneOf){
|
|
75
|
+
objDef = addSemicolonIfMissing(objDef);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Keep single line */
|
|
79
|
+
if (isOneOf){
|
|
80
|
+
objDef = objDef.split("\n").join("");
|
|
81
|
+
}
|
|
82
|
+
return nullType + objDef;
|
|
83
|
+
|
|
84
|
+
} else if (fieldType?.enum) {
|
|
85
|
+
return nullType + fieldType.enum.map(v => asValue(v)).join(" | ");
|
|
86
|
+
|
|
87
|
+
} else if (fieldType?.oneOf || fieldType?.oneOfType) {
|
|
88
|
+
const oneOf = fieldType?.oneOf || fieldType?.oneOfType.map(type => ({ type }));
|
|
89
|
+
return (fieldType.nullable ? `\n${innerLeading} | null` : "") + oneOf.map(v => `\n${innerLeading} | ` + getFieldType(v, true, undefined, depth + 1)).join("");
|
|
90
|
+
|
|
91
|
+
} else if (fieldType?.arrayOf || fieldType?.arrayOfType) {
|
|
92
|
+
const arrayOf = fieldType?.arrayOf || { type: fieldType?.arrayOfType };
|
|
93
|
+
return `${fieldType.nullable ? `null | ` : ""} ( ${getFieldType(arrayOf, true, undefined, depth + 1)} )[]`;
|
|
94
|
+
|
|
95
|
+
} else if (fieldType?.record) {
|
|
96
|
+
const { keysEnum, values, partial } = fieldType.record;
|
|
97
|
+
// TODO: ensure props with undefined values are not allowed in the TS type (strict union)
|
|
98
|
+
const getRecord = (v: string) => partial? `Partial<Record<${v}>>` : `Record<${v}>`;
|
|
99
|
+
return `${fieldType.nullable ? `null |` : ""} ${getRecord(`${keysEnum?.map(v => asValue(v)).join(" | ") ?? "string"}, ${!values? "any" : getFieldType(values, true, undefined, depth + 1)}`)}`
|
|
100
|
+
|
|
101
|
+
} else if(fieldType?.lookup){
|
|
102
|
+
|
|
103
|
+
const l = fieldType.lookup;
|
|
104
|
+
if(l.type === "data-def"){
|
|
105
|
+
return `${fieldType.nullable ? `null |` : ""} ${
|
|
106
|
+
getFieldType({
|
|
107
|
+
type: {
|
|
108
|
+
table: "string",
|
|
109
|
+
column: "string",
|
|
110
|
+
filter: { record: {}, optional: true },
|
|
111
|
+
isArray: { type: "boolean", optional: true },
|
|
112
|
+
searchColumns: { type: "string[]", optional: true },
|
|
113
|
+
isFullRow: { optional: true, type: {
|
|
114
|
+
displayColumns:{ type: "string[]", optional: true }
|
|
115
|
+
}},
|
|
116
|
+
showInRowCard: { optional: true, record: {} }
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const isSChema = l.type === "schema";
|
|
123
|
+
let type = isSChema? (l.object === "table"? "string" : `{ "table": string; "column": string; }`) : "";
|
|
124
|
+
if(!isSChema){
|
|
125
|
+
const cols = tables.find(t => t.name === l.table)?.columns
|
|
126
|
+
if(!l.isFullRow){
|
|
127
|
+
type = postgresToTsType(cols?.find(c => c.name === l.column)?.udt_name ?? "text");
|
|
128
|
+
} else {
|
|
129
|
+
type = !cols? "any" : `{ ${cols.map(c => `${JSON.stringify(c.name)}: ${c.is_nullable? "null | " : "" } ${postgresToTsType(c.udt_name)}; `).join(" ")} }`
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return `${fieldType.nullable ? `null | ` : ""}${type}${l.isArray? "[]" : ""}`;
|
|
133
|
+
|
|
134
|
+
} else throw "Unexpected getSchemaTSTypes: " + JSON.stringify({ fieldType, schema }, null, 2)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return getFieldType({ ...schema as any, nullable: colOpts.nullable }, undefined, outerLeading);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const isValidIdentifier = (str: string) => {
|
|
141
|
+
const identifierRegex = /^[A-Za-z$_][A-Za-z0-9$_]*$/;
|
|
142
|
+
return identifierRegex.test(str);
|
|
143
|
+
}
|