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.
Files changed (107) hide show
  1. package/dist/Auth/setEmailProvider.js +2 -2
  2. package/dist/Auth/setEmailProvider.js.map +1 -1
  3. package/lib/Auth/AuthHandler.ts +436 -0
  4. package/lib/Auth/AuthTypes.ts +280 -0
  5. package/lib/Auth/getSafeReturnURL.ts +35 -0
  6. package/lib/Auth/sendEmail.ts +83 -0
  7. package/lib/Auth/setAuthProviders.ts +128 -0
  8. package/lib/Auth/setEmailProvider.ts +85 -0
  9. package/lib/Auth/setupAuthRoutes.ts +161 -0
  10. package/lib/DBEventsManager.ts +178 -0
  11. package/lib/DBSchemaBuilder.ts +225 -0
  12. package/lib/DboBuilder/DboBuilder.ts +319 -0
  13. package/lib/DboBuilder/DboBuilderTypes.ts +361 -0
  14. package/lib/DboBuilder/QueryBuilder/Functions.ts +1153 -0
  15. package/lib/DboBuilder/QueryBuilder/QueryBuilder.ts +288 -0
  16. package/lib/DboBuilder/QueryBuilder/getJoinQuery.ts +263 -0
  17. package/lib/DboBuilder/QueryBuilder/getNewQuery.ts +271 -0
  18. package/lib/DboBuilder/QueryBuilder/getSelectQuery.ts +136 -0
  19. package/lib/DboBuilder/QueryBuilder/prepareHaving.ts +22 -0
  20. package/lib/DboBuilder/QueryStreamer.ts +250 -0
  21. package/lib/DboBuilder/TableHandler/DataValidator.ts +428 -0
  22. package/lib/DboBuilder/TableHandler/TableHandler.ts +205 -0
  23. package/lib/DboBuilder/TableHandler/delete.ts +115 -0
  24. package/lib/DboBuilder/TableHandler/insert.ts +183 -0
  25. package/lib/DboBuilder/TableHandler/insertTest.ts +78 -0
  26. package/lib/DboBuilder/TableHandler/onDeleteFromFileTable.ts +62 -0
  27. package/lib/DboBuilder/TableHandler/runInsertUpdateQuery.ts +134 -0
  28. package/lib/DboBuilder/TableHandler/update.ts +126 -0
  29. package/lib/DboBuilder/TableHandler/updateBatch.ts +49 -0
  30. package/lib/DboBuilder/TableHandler/updateFile.ts +48 -0
  31. package/lib/DboBuilder/TableHandler/upsert.ts +34 -0
  32. package/lib/DboBuilder/ViewHandler/ViewHandler.ts +393 -0
  33. package/lib/DboBuilder/ViewHandler/count.ts +38 -0
  34. package/lib/DboBuilder/ViewHandler/find.ts +153 -0
  35. package/lib/DboBuilder/ViewHandler/getExistsCondition.ts +73 -0
  36. package/lib/DboBuilder/ViewHandler/getExistsFilters.ts +74 -0
  37. package/lib/DboBuilder/ViewHandler/getInfo.ts +32 -0
  38. package/lib/DboBuilder/ViewHandler/getTableJoinQuery.ts +84 -0
  39. package/lib/DboBuilder/ViewHandler/parseComplexFilter.ts +96 -0
  40. package/lib/DboBuilder/ViewHandler/parseFieldFilter.ts +105 -0
  41. package/lib/DboBuilder/ViewHandler/parseJoinPath.ts +208 -0
  42. package/lib/DboBuilder/ViewHandler/prepareSortItems.ts +163 -0
  43. package/lib/DboBuilder/ViewHandler/prepareWhere.ts +90 -0
  44. package/lib/DboBuilder/ViewHandler/size.ts +37 -0
  45. package/lib/DboBuilder/ViewHandler/subscribe.ts +118 -0
  46. package/lib/DboBuilder/ViewHandler/validateViewRules.ts +70 -0
  47. package/lib/DboBuilder/dboBuilderUtils.ts +222 -0
  48. package/lib/DboBuilder/getColumns.ts +114 -0
  49. package/lib/DboBuilder/getCondition.ts +201 -0
  50. package/lib/DboBuilder/getSubscribeRelatedTables.ts +190 -0
  51. package/lib/DboBuilder/getTablesForSchemaPostgresSQL.ts +426 -0
  52. package/lib/DboBuilder/insertNestedRecords.ts +355 -0
  53. package/lib/DboBuilder/parseUpdateRules.ts +187 -0
  54. package/lib/DboBuilder/prepareShortestJoinPaths.ts +186 -0
  55. package/lib/DboBuilder/runSQL.ts +182 -0
  56. package/lib/DboBuilder/runTransaction.ts +50 -0
  57. package/lib/DboBuilder/sqlErrCodeToMsg.ts +254 -0
  58. package/lib/DboBuilder/uploadFile.ts +69 -0
  59. package/lib/Event_Trigger_Tags.ts +118 -0
  60. package/lib/FileManager/FileManager.ts +358 -0
  61. package/lib/FileManager/getValidatedFileType.ts +69 -0
  62. package/lib/FileManager/initFileManager.ts +187 -0
  63. package/lib/FileManager/upload.ts +62 -0
  64. package/lib/FileManager/uploadStream.ts +79 -0
  65. package/lib/Filtering.ts +463 -0
  66. package/lib/JSONBValidation/validate_jsonb_schema_sql.ts +502 -0
  67. package/lib/JSONBValidation/validation.ts +143 -0
  68. package/lib/Logging.ts +127 -0
  69. package/lib/PostgresNotifListenManager.ts +143 -0
  70. package/lib/Prostgles.ts +485 -0
  71. package/lib/ProstglesTypes.ts +196 -0
  72. package/lib/PubSubManager/PubSubManager.ts +609 -0
  73. package/lib/PubSubManager/addSub.ts +138 -0
  74. package/lib/PubSubManager/addSync.ts +141 -0
  75. package/lib/PubSubManager/getCreatePubSubManagerError.ts +72 -0
  76. package/lib/PubSubManager/getPubSubManagerInitQuery.ts +662 -0
  77. package/lib/PubSubManager/initPubSubManager.ts +79 -0
  78. package/lib/PubSubManager/notifListener.ts +173 -0
  79. package/lib/PubSubManager/orphanTriggerCheck.ts +70 -0
  80. package/lib/PubSubManager/pushSubData.ts +55 -0
  81. package/lib/PublishParser/PublishParser.ts +162 -0
  82. package/lib/PublishParser/getFileTableRules.ts +124 -0
  83. package/lib/PublishParser/getSchemaFromPublish.ts +141 -0
  84. package/lib/PublishParser/getTableRulesWithoutFileTable.ts +177 -0
  85. package/lib/PublishParser/publishTypesAndUtils.ts +399 -0
  86. package/lib/RestApi.ts +127 -0
  87. package/lib/SchemaWatch/SchemaWatch.ts +90 -0
  88. package/lib/SchemaWatch/createSchemaWatchEventTrigger.ts +3 -0
  89. package/lib/SchemaWatch/getValidatedWatchSchemaType.ts +45 -0
  90. package/lib/SchemaWatch/getWatchSchemaTagList.ts +27 -0
  91. package/lib/SyncReplication.ts +557 -0
  92. package/lib/TableConfig/TableConfig.ts +468 -0
  93. package/lib/TableConfig/getColumnDefinitionQuery.ts +111 -0
  94. package/lib/TableConfig/getConstraintDefinitionQueries.ts +95 -0
  95. package/lib/TableConfig/getFutureTableSchema.ts +64 -0
  96. package/lib/TableConfig/getPGIndexes.ts +53 -0
  97. package/lib/TableConfig/getTableColumnQueries.ts +129 -0
  98. package/lib/TableConfig/initTableConfig.ts +326 -0
  99. package/lib/index.ts +13 -0
  100. package/lib/initProstgles.ts +319 -0
  101. package/lib/onSocketConnected.ts +102 -0
  102. package/lib/runClientRequest.ts +129 -0
  103. package/lib/shortestPath.ts +122 -0
  104. package/lib/typeTests/DBoGenerated.d.ts +320 -0
  105. package/lib/typeTests/dboTypeCheck.ts +81 -0
  106. package/lib/utils.ts +15 -0
  107. 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
+ }